Cli Restructure + Multi-Mask Select on Extract (#1012)

- Split up cli.py to smaller modules
- Enable Multi Mask Selection in Extraction
- Handle multi option selection options in the GUI
- Document lib/cli
This commit is contained in:
torzdf 2020-04-22 00:04:21 +01:00 committed by GitHub
parent add55ccb3f
commit ff8d85118e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 2014 additions and 1411 deletions

65
docs/full/lib/cli.rst Normal file
View File

@ -0,0 +1,65 @@
***********
cli package
***********
The CLI Package handles the Command Line Arguments that act as the entry point into Faceswap.
.. contents:: Contents
:local:
args module
===========
.. rubric:: Module Summary
.. autosummary::
:nosignatures:
~lib.cli.args.ConvertArgs
~lib.cli.args.ExtractArgs
~lib.cli.args.ExtractConvertArgs
~lib.cli.args.FaceSwapArgs
~lib.cli.args.FullHelpArgumentParser
~lib.cli.args.GuiArgs
~lib.cli.args.SmartFormatter
~lib.cli.args.TrainArgs
.. rubric:: Module
.. automodule:: lib.cli.args
:members:
:undoc-members:
:show-inheritance:
actions module
==============
.. rubric:: Module Summary
.. autosummary::
:nosignatures:
~lib.cli.actions.ContextFullPaths
~lib.cli.actions.DirFullPaths
~lib.cli.actions.DirOrFileFullPaths
~lib.cli.actions.FileFullPaths
~lib.cli.actions.FilesFullPaths
~lib.cli.actions.MultiOption
~lib.cli.actions.Radio
~lib.cli.actions.SaveFileFullPaths
~lib.cli.actions.Slider
.. rubric:: Module
.. automodule:: lib.cli.actions
:members:
:undoc-members:
:show-inheritance:
launcher module
===============
.. automodule:: lib.cli.launcher
:members:
:undoc-members:
:show-inheritance:

View File

@ -3,7 +3,7 @@ gui package
***********
The GUI Package contains the entire code base for Faceswap's optional GUI. The GUI itself itself
is largely self-generated from the command line options specified in :mod:`lib.cli`.
is largely self-generated from the command line options specified in :mod:`lib.cli.args`.
.. contents:: Contents
:local:
@ -18,6 +18,7 @@ custom\_widgets module
~lib.gui.custom_widgets.ConsoleOut
~lib.gui.custom_widgets.ContextMenu
~lib.gui.custom_widgets.MultiOption
~lib.gui.custom_widgets.RightClickMenu
~lib.gui.custom_widgets.StatusBar
~lib.gui.custom_widgets.Tooltip

View File

@ -2,7 +2,7 @@
""" The master faceswap.py script """
import sys
import lib.cli as cli
from lib.cli import args
from lib.config import generate_configs
if sys.version_info[0] < 3:
@ -11,28 +11,37 @@ if sys.version_info[0] == 3 and sys.version_info[1] < 6:
raise Exception("This program requires at least python3.6")
def bad_args(args):
""" Print help on bad arguments """
PARSER.print_help()
_PARSER = args.FullHelpArgumentParser()
def _bad_args():
""" Print help to console when bad arguments are provided. """
_PARSER.print_help()
sys.exit(0)
if __name__ == "__main__":
def _main():
""" The main entry point into Faceswap.
- Generates the config files, if they don't pre-exist.
- Compiles the :class:`~lib.cli.args.FullHelpArgumentParser` objects for each section of
Faceswap.
- Sets the default values and launches the relevant script.
- Outputs help if invalid parameters are provided.
"""
generate_configs()
PARSER = cli.FullHelpArgumentParser()
SUBPARSER = PARSER.add_subparsers()
EXTRACT = cli.ExtractArgs(SUBPARSER,
"extract",
"Extract the faces from pictures")
TRAIN = cli.TrainArgs(SUBPARSER,
"train",
"This command trains the model for the two faces A and B")
CONVERT = cli.ConvertArgs(SUBPARSER,
"convert",
"Convert a source image to a new one with the face swapped")
GUI = cli.GuiArgs(SUBPARSER,
"gui",
"Launch the Faceswap Graphical User Interface")
PARSER.set_defaults(func=bad_args)
ARGUMENTS = PARSER.parse_args()
ARGUMENTS.func(ARGUMENTS)
subparser = _PARSER.add_subparsers()
args.ExtractArgs(subparser, "extract", "Extract the faces from pictures")
args.TrainArgs(subparser, "train", "This command trains the model for the two faces A and B")
args.ConvertArgs(subparser,
"convert",
"Convert a source image to a new one with the face swapped")
args.GuiArgs(subparser, "gui", "Launch the Faceswap Graphical User Interface")
_PARSER.set_defaults(func=_bad_args)
arguments = _PARSER.parse_args()
arguments.func(arguments)
if __name__ == "__main__":
_main()

1309
lib/cli.py

File diff suppressed because it is too large Load Diff

0
lib/cli/__init__.py Normal file
View File

367
lib/cli/actions.py Normal file
View File

@ -0,0 +1,367 @@
#!/usr/bin/env python3
""" Custom :class:`argparse.Action` objects for Faceswap's Command Line Interface.
The custom actions within this module allow for custom manipulation of Command Line Arguments
as well as adding a mechanism for indicating to the GUI how specific options should be rendered.
"""
import argparse
import os
# << FILE HANDLING >>
class _FullPaths(argparse.Action): # pylint: disable=too-few-public-methods
""" Parent class for various file type and file path handling classes.
Expands out given paths to their full absolute paths. This class should not be
called directly. It is the base class for the various different file handling
methods.
"""
def __call__(self, parser, namespace, values, option_string=None):
if isinstance(values, (list, tuple)):
vals = [os.path.abspath(os.path.expanduser(val)) for val in values]
else:
vals = os.path.abspath(os.path.expanduser(values))
setattr(namespace, self.dest, vals)
class DirFullPaths(_FullPaths):
""" Adds support for a Directory browser in the GUI.
This is a standard :class:`argparse.Action` (with stock parameters) which indicates to the GUI
that a dialog box should be opened in order to browse for a folder.
No additional parameters are required.
Example
-------
>>> argument_list = []
>>> argument_list.append(dict(
>>> opts=("-f", "--folder_location"),
>>> action=DirFullPaths)),
"""
# pylint: disable=too-few-public-methods,unnecessary-pass
pass
class FileFullPaths(_FullPaths):
""" Adds support for a File browser to select a single file in the GUI.
This extends the standard :class:`argparse.Action` and adds an additional parameter
:attr:`filetypes`, indicating to the GUI that it should pop a file browser for opening a file
and limit the results to the file types listed. As well as the standard parameters, the
following parameter is required:
Parameters
----------
filetypes: str
The accepted file types for this option. This is the key for the GUIs lookup table which
can be found in :class:`lib.gui.utils.FileHandler`
Example
-------
>>> argument_list = []
>>> argument_list.append(dict(
>>> opts=("-f", "--video_location"),
>>> action=FileFullPaths,
>>> filetypes="video))"
"""
# pylint: disable=too-few-public-methods
def __init__(self, *args, filetypes=None, **kwargs):
super().__init__(*args, **kwargs)
self.filetypes = filetypes
def _get_kwargs(self):
names = ["option_strings",
"dest",
"nargs",
"const",
"default",
"type",
"choices",
"help",
"metavar",
"filetypes"]
return [(name, getattr(self, name)) for name in names]
class FilesFullPaths(FileFullPaths): # pylint: disable=too-few-public-methods
""" Adds support for a File browser to select multiple files in the GUI.
This extends the standard :class:`argparse.Action` and adds an additional parameter
:attr:`filetypes`, indicating to the GUI that it should pop a file browser, and limit
the results to the file types listed. Multiple files can be selected for opening, so the
:attr:`nargs` parameter must be set. As well as the standard parameters, the following
parameter is required:
Parameters
----------
filetypes: str
The accepted file types for this option. This is the key for the GUIs lookup table which
can be found in :class:`lib.gui.utils.FileHandler`
Example
-------
>>> argument_list = []
>>> argument_list.append(dict(
>>> opts=("-f", "--images"),
>>> action=FilesFullPaths,
>>> filetypes="image",
>>> nargs="+"))
"""
def __init__(self, *args, filetypes=None, **kwargs):
if kwargs.get("nargs", None) is None:
opt = kwargs["option_strings"]
raise ValueError("nargs must be provided for FilesFullPaths: {}".format(opt))
super().__init__(*args, **kwargs)
class DirOrFileFullPaths(FileFullPaths): # pylint: disable=too-few-public-methods
""" Adds support to the GUI to launch either a file browser or a folder browser.
Some inputs (for example source frames) can come from a folder of images or from a
video file. This indicates to the GUI that it should place 2 buttons (one for a folder
browser, one for a file browser) for file/folder browsing.
The standard :class:`argparse.Action` is extended with the additional parameter
:attr:`filetypes`, indicating to the GUI that it should pop a file browser, and limit
the results to the file types listed. As well as the standard parameters, the following
parameter is required:
Parameters
----------
filetypes: str
The accepted file types for this option. This is the key for the GUIs lookup table which
can be found in :class:`lib.gui.utils.FileHandler`. NB: This parameter is only used for
the file browser and not the folder browser
Example
-------
>>> argument_list = []
>>> argument_list.append(dict(
>>> opts=("-f", "--input_frames"),
>>> action=DirOrFileFullPaths,
>>> filetypes="video))"
"""
pass # pylint: disable=unnecessary-pass
class SaveFileFullPaths(FileFullPaths):
""" Adds support for a Save File dialog in the GUI.
This extends the standard :class:`argparse.Action` and adds an additional parameter
:attr:`filetypes`, indicating to the GUI that it should pop a save file browser, and limit
the results to the file types listed. As well as the standard parameters, the following
parameter is required:
Parameters
----------
filetypes: str
The accepted file types for this option. This is the key for the GUIs lookup table which
can be found in :class:`lib.gui.utils.FileHandler`
Example
-------
>>> argument_list = []
>>> argument_list.append(dict(
>>> opts=("-f", "--video_out"),
>>> action=SaveFileFullPaths,
>>> filetypes="video"))
"""
# pylint: disable=too-few-public-methods,unnecessary-pass
pass
class ContextFullPaths(FileFullPaths):
""" Adds support for context sensitive browser dialog opening in the GUI.
For some tasks, the type of action (file load, folder open, file save etc.) can vary
depending on the task to be performed (a good example of this is the effmpeg tool).
Using this action indicates to the GUI that the type of dialog to be launched can change
depending on another option. As well as the standard parameters, the below parameters are
required. NB: :attr:`nargs` are explicitly disallowed.
Parameters
----------
filetypes: str
The accepted file types for this option. This is the key for the GUIs lookup table which
can be found in :class:`lib.gui.utils.FileHandler`
action_option: str
The command line option that dictates the context of the file dialog to be opened.
Bespoke actions are set in :class:`lib.gui.utils.FileHandler`
Example
-------
Assuming an argument has already been set with option string `-a` indicating the action to be
performed, the following will pop a different type of dialog depending on the action selected:
>>> argument_list = []
>>> argument_list.append(dict(
>>> opts=("-f", "--input_video"),
>>> action=ContextFullPaths,
>>> filetypes="video",
>>> action_option="-a"))
"""
# pylint: disable=too-few-public-methods, too-many-arguments
def __init__(self, *args, filetypes=None, action_option=None, **kwargs):
opt = kwargs["option_strings"]
if kwargs.get("nargs", None) is not None:
raise ValueError("nargs not allowed for ContextFullPaths: {}".format(opt))
if filetypes is None:
raise ValueError("filetypes is required for ContextFullPaths: {}".format(opt))
if action_option is None:
raise ValueError("action_option is required for ContextFullPaths: {}".format(opt))
super().__init__(*args, filetypes=filetypes, **kwargs)
self.action_option = action_option
def _get_kwargs(self):
names = ["option_strings",
"dest",
"nargs",
"const",
"default",
"type",
"choices",
"help",
"metavar",
"filetypes",
"action_option"]
return [(name, getattr(self, name)) for name in names]
# << GUI DISPLAY OBJECTS >>
class Radio(argparse.Action): # pylint: disable=too-few-public-methods
""" Adds support for a GUI Radio options box.
This is a standard :class:`argparse.Action` (with stock parameters) which indicates to the GUI
that the options passed should be rendered as a group of Radio Buttons rather than a combo box.
No additional parameters are required, but the :attr:`choices` parameter must be provided as
these will be the Radio Box options. :attr:`nargs` are explicitly disallowed.
Example
-------
>>> argument_list = []
>>> argument_list.append(dict(
>>> opts=("-f", "--foobar"),
>>> action=Radio,
>>> choices=["foo", "bar"))
"""
def __init__(self, *args, **kwargs):
opt = kwargs["option_strings"]
if kwargs.get("nargs", None) is not None:
raise ValueError("nargs not allowed for Radio buttons: {}".format(opt))
if not kwargs.get("choices", []):
raise ValueError("Choices must be provided for Radio buttons: {}".format(opt))
super().__init__(*args, **kwargs)
def __call__(self, parser, namespace, values, option_string=None):
setattr(namespace, self.dest, values)
class MultiOption(argparse.Action): # pylint: disable=too-few-public-methods
""" Adds support for multiple option checkboxes in the GUI.
This is a standard :class:`argparse.Action` (with stock parameters) which indicates to the GUI
that the options passed should be rendered as a group of Radio Buttons rather than a combo box.
The :attr:`choices` parameter must be provided as this provides the valid option choices.
Example
-------
>>> argument_list = []
>>> argument_list.append(dict(
>>> opts=("-f", "--foobar"),
>>> action=MultiOption,
>>> choices=["foo", "bar"))
"""
def __init__(self, *args, **kwargs):
opt = kwargs["option_strings"]
if not kwargs.get("nargs", []):
raise ValueError("nargs must be provided for MultiOption: {}".format(opt))
if not kwargs.get("choices", []):
raise ValueError("Choices must be provided for MultiOption: {}".format(opt))
super().__init__(*args, **kwargs)
def __call__(self, parser, namespace, values, option_string=None):
setattr(namespace, self.dest, values)
class Slider(argparse.Action): # pylint: disable=too-few-public-methods
""" Adds support for a slider in the GUI.
The standard :class:`argparse.Action` is extended with the additional parameters listed below.
The :attr:`default` value must be supplied and the :attr:`type` must be either :class:`int` or
:class:`float`. :attr:`nargs` are explicitly disallowed.
Parameters
----------
min_max: tuple
The (`min`, `max`) values that the slider's range should be set to. The values should be a
pair of `float` or `int` data types, depending on the data type of the slider. NB: These
min/max values are not enforced, they are purely for setting the slider range. Values
outside of this range can still be explicitly passed in from the cli.
rounding: int
If the underlying data type for the option is a `float` then this value is the number of
decimal places to round the slider values to. If the underlying data type for the option is
an `int` then this is the step interval between each value for the slider.
Examples
--------
For integer values:
>>> argument_list = []
>>> argument_list.append(dict(
>>> opts=("-f", "--foobar"),
>>> action=Slider,
>>> min_max=(0, 10)
>>> rounding=1
>>> type=int,
>>> default=5))
For floating point values:
>>> argument_list = []
>>> argument_list.append(dict(
>>> opts=("-f", "--foobar"),
>>> action=Slider,
>>> min_max=(0.00, 1.00)
>>> rounding=2
>>> type=float,
>>> default=5.00))
"""
def __init__(self, *args, min_max=None, rounding=None, **kwargs):
opt = kwargs["option_strings"]
if kwargs.get("nargs", None) is not None:
raise ValueError("nargs not allowed for Slider: {}".format(opt))
if kwargs.get("default", None) is None:
raise ValueError("A default value must be supplied for Slider: {}".format(opt))
if kwargs.get("type", None) not in (int, float):
raise ValueError("Sliders only accept int and float data types: {}".format(opt))
if min_max is None:
raise ValueError("min_max must be provided for Sliders: {}".format(opt))
if rounding is None:
raise ValueError("rounding must be provided for Sliders: {}".format(opt))
super().__init__(*args, **kwargs)
self.min_max = min_max
self.rounding = rounding
def _get_kwargs(self):
names = ["option_strings",
"dest",
"nargs",
"const",
"default",
"type",
"choices",
"help",
"metavar",
"min_max", # Tuple containing min and max values of scale
"rounding"] # Decimal places to round floats to or step interval for ints
return [(name, getattr(self, name)) for name in names]
def __call__(self, parser, namespace, values, option_string=None):
setattr(namespace, self.dest, values)

1138
lib/cli/args.py Normal file

File diff suppressed because it is too large Load Diff

193
lib/cli/launcher.py Normal file
View File

@ -0,0 +1,193 @@
#!/usr/bin/env python3
""" Launches the correct script with the given Command Line Arguments """
import logging
import os
import platform
import sys
from importlib import import_module
from lib.logger import crash_log, log_setup
from lib.utils import FaceswapError, get_backend, safe_shutdown, set_system_verbosity
logger = logging.getLogger(__name__) # pylint: disable=invalid-name
class ScriptExecutor(): # pylint:disable=too-few-public-methods
""" Loads the relevant script modules and executes the script.
This class is initialized in each of the argparsers for the relevant
command, then execute script is called within their set_default
function.
Parameters
----------
command: str
The faceswap command that is being executed
"""
def __init__(self, command):
self._command = command.lower()
def _import_script(self):
""" Imports the relevant script as indicated by :attr:`_command` from the scripts folder.
Returns
-------
class: Faceswap Script
The uninitialized script from the faceswap scripts folder.
"""
self._test_for_tf_version()
self._test_for_gui()
cmd = os.path.basename(sys.argv[0])
src = "tools.{}".format(self._command.lower()) if cmd == "tools.py" else "scripts"
mod = ".".join((src, self._command.lower()))
module = import_module(mod)
script = getattr(module, self._command.title())
return script
@staticmethod
def _test_for_tf_version():
""" Check that the required Tensorflow version is installed.
Raises
------
FaceswapError
If Tensorflow is not found, or is not between versions 1.12 and 1.15
"""
min_ver = 1.12
max_ver = 1.15
try:
# Ensure tensorflow doesn't pin all threads to one core when using Math Kernel Library
os.environ["KMP_AFFINITY"] = "disabled"
import tensorflow as tf # pylint:disable=import-outside-toplevel
except ImportError as err:
raise FaceswapError("There was an error importing Tensorflow. This is most likely "
"because you do not have TensorFlow installed, or you are trying "
"to run tensorflow-gpu on a system without an Nvidia graphics "
"card. Original import error: {}".format(str(err)))
tf_ver = float(".".join(tf.__version__.split(".")[:2])) # pylint:disable=no-member
if tf_ver < min_ver:
raise FaceswapError("The minimum supported Tensorflow is version {} but you have "
"version {} installed. Please upgrade Tensorflow.".format(
min_ver, tf_ver))
if tf_ver > max_ver:
raise FaceswapError("The maximumum supported Tensorflow is version {} but you have "
"version {} installed. Please downgrade Tensorflow.".format(
max_ver, tf_ver))
logger.debug("Installed Tensorflow Version: %s", tf_ver)
def _test_for_gui(self):
""" If running the gui, performs check to ensure necessary prerequisites are present. """
if self._command != "gui":
return
self._test_tkinter()
self._check_display()
@staticmethod
def _test_tkinter():
""" If the user is running the GUI, test whether the tkinter app is available on their
machine. If not exit gracefully.
This avoids having to import every tkinter function within the GUI in a wrapper and
potentially spamming traceback errors to console.
Raises
------
FaceswapError
If tkinter cannot be imported
"""
try:
# pylint: disable=unused-variable
import tkinter # noqa pylint: disable=unused-import,import-outside-toplevel
except ImportError:
logger.error("It looks like TkInter isn't installed for your OS, so the GUI has been "
"disabled. To enable the GUI please install the TkInter application. You "
"can try:")
logger.info("Anaconda: conda install tk")
logger.info("Windows/macOS: Install ActiveTcl Community Edition from "
"http://www.activestate.com")
logger.info("Ubuntu/Mint/Debian: sudo apt install python3-tk")
logger.info("Arch: sudo pacman -S tk")
logger.info("CentOS/Redhat: sudo yum install tkinter")
logger.info("Fedora: sudo dnf install python3-tkinter")
raise FaceswapError("TkInter not found")
@staticmethod
def _check_display():
""" Check whether there is a display to output the GUI to.
If running on Windows then it is assumed that we are not running in headless mode
Raises
------
FaceswapError
If a DISPLAY environmental cannot be found
"""
if not os.environ.get("DISPLAY", None) and os.name != "nt":
if platform.system() == "Darwin":
logger.info("macOS users need to install XQuartz. "
"See https://support.apple.com/en-gb/HT201341")
raise FaceswapError("No display detected. GUI mode has been disabled.")
def execute_script(self, arguments):
""" Performs final set up and launches the requested :attr:`_command` with the given
command line arguments.
Monitors for errors and attempts to shut down the process cleanly on exit.
Parameters
----------
arguments: :class:`argparse.Namespace`
The command line arguments to be passed to the executing script.
"""
set_system_verbosity(arguments.loglevel)
is_gui = hasattr(arguments, "redirect_gui") and arguments.redirect_gui
log_setup(arguments.loglevel, arguments.logfile, self._command, is_gui)
logger.debug("Executing: %s. PID: %s", self._command, os.getpid())
success = False
if get_backend() == "amd":
plaidml_found = self._setup_amd(arguments.loglevel)
if not plaidml_found:
safe_shutdown(got_error=True)
return
try:
script = self._import_script()
process = script(arguments)
process.process()
success = True
except FaceswapError as err:
for line in str(err).splitlines():
logger.error(line)
except KeyboardInterrupt: # pylint: disable=try-except-raise
raise
except SystemExit:
pass
except Exception: # pylint: disable=broad-except
crash_file = crash_log()
logger.exception("Got Exception on main handler:")
logger.critical("An unexpected crash has occurred. Crash report written to '%s'. "
"You MUST provide this file if seeking assistance. Please verify you "
"are running the latest version of faceswap before reporting",
crash_file)
finally:
safe_shutdown(got_error=not success)
@staticmethod
def _setup_amd(log_level):
""" Test for plaidml and perform setup for AMD.
Parameters
----------
log_level: str
The requested log level to run at
"""
logger.debug("Setting up for AMD")
try:
import plaidml # noqa pylint:disable=unused-import,import-outside-toplevel
except ImportError:
logger.error("PlaidML not found. Run `pip install plaidml-keras` for AMD support")
return False
from lib.plaidml_tools import setup_plaidml # pylint:disable=import-outside-toplevel
setup_plaidml(log_level)
logger.debug("setup up for PlaidML")
return True

View File

@ -10,8 +10,7 @@ from functools import partial
from _tkinter import Tcl_Obj, TclError
from .custom_widgets import ContextMenu
from .custom_widgets import Tooltip
from .custom_widgets import ContextMenu, MultiOption, Tooltip
from .utils import FileHandler, get_config, get_images
logger = logging.getLogger(__name__) # pylint: disable=invalid-name
@ -95,6 +94,8 @@ class ControlPanelOption():
Used for combo boxes and radio control option setting
is_radio: bool, optional
Specifies to use a Radio control instead of combobox if choices are passed
is_multi_option:
Specifies to use a Multi Check Button option group for the specified control
rounding: int or float, optional
For slider controls. Sets the stepping
min_max: int or float, optional
@ -113,13 +114,14 @@ class ControlPanelOption():
def __init__(self, title, dtype, # pylint:disable=too-many-arguments
group=None, default=None, initial_value=None, choices=None, is_radio=False,
rounding=None, min_max=None, sysbrowser=None, helptext=None,
track_modified=False, command=None):
is_multi_option=False, rounding=None, min_max=None, sysbrowser=None,
helptext=None, track_modified=False, command=None):
logger.debug("Initializing %s: (title: '%s', dtype: %s, group: %s, default: %s, "
"initial_value: %s, choices: %s, is_radio: %s, rounding: %s, min_max: %s, "
"sysbrowser: %s, helptext: '%s', track_modified: %s, command: '%s')",
self.__class__.__name__, title, dtype, group, default, initial_value, choices,
is_radio, rounding, min_max, sysbrowser, helptext, track_modified, command)
"initial_value: %s, choices: %s, is_radio: %s, is_multi_option: %s, "
"rounding: %s, min_max: %s, sysbrowser: %s, helptext: '%s', "
"track_modified: %s, command: '%s')", self.__class__.__name__, title, dtype,
group, default, initial_value, choices, is_radio, is_multi_option, rounding,
min_max, sysbrowser, helptext, track_modified, command)
self.dtype = dtype
self.sysbrowser = sysbrowser
@ -130,6 +132,7 @@ class ControlPanelOption():
initial_value=initial_value,
choices=choices,
is_radio=is_radio,
is_multi_option=is_multi_option,
rounding=rounding,
min_max=min_max,
helptext=helptext)
@ -176,6 +179,12 @@ class ControlPanelOption():
""" Return is_radio """
return self._options["is_radio"]
@property
def is_multi_option(self):
""" bool: ``True`` if the control should be contained in a multi check button group,
otherwise ``False``. """
return self._options["is_multi_option"]
@property
def rounding(self):
""" Return rounding """
@ -241,13 +250,15 @@ class ControlPanelOption():
def get_control(self):
""" Set the correct control type based on the datatype or for this option """
if self.choices and self.is_radio:
control = ttk.Radiobutton
control = "radio"
elif self.choices and self.is_multi_option:
control = "multi"
elif self.choices:
control = ttk.Combobox
elif self.dtype == bool:
control = ttk.Checkbutton
elif self.dtype in (int, float):
control = ttk.Scale
control = "scale"
else:
control = ttk.Entry
logger.debug("Setting control '%s' to %s", self.title, control)
@ -590,7 +601,9 @@ class AutoFillContainer():
"pack_info": self.pack_config_cleaner(child),
"name": child.winfo_name(),
"config": self.config_cleaner(child),
"children": self.get_all_children_config(child, [])}
"children": self.get_all_children_config(child, []),
# Some children have custom kwargs, so keep dicts in sync
"custom_kwargs": dict()}
for idx, child in enumerate(children)]
logger.debug("Compiled AutoFillContainer children: %s", self._widget_config)
@ -599,6 +612,15 @@ class AutoFillContainer():
for child in widget.winfo_children():
if child.winfo_ismapped():
id_ = str(child)
if child.__class__.__name__ == "MultiOption":
# MultiOption checkbox groups are a custom object with additional parameter
# requirements.
custom_kwargs = dict(
value=child._value, # pylint:disable=protected-access
variable=child._master_variable) # pylint:disable=protected-access
else:
custom_kwargs = dict()
child_list.append({
"class": child.__class__,
"id": id_,
@ -607,7 +629,8 @@ class AutoFillContainer():
"pack_info": self.pack_config_cleaner(child),
"name": child.winfo_name(),
"config": self.config_cleaner(child),
"parent": child.winfo_parent()})
"parent": child.winfo_parent(),
"custom_kwargs": custom_kwargs})
self.get_all_children_config(child, child_list)
return child_list
@ -668,7 +691,9 @@ class AutoFillContainer():
else:
# Get the next sub-frame if this doesn't have a logged parent
parent = self.subframe
clone = widget_dict["class"](parent, name=widget_dict["name"])
clone = widget_dict["class"](parent,
name=widget_dict["name"],
**widget_dict["custom_kwargs"])
if widget_dict["config"] is not None:
clone.configure(**widget_dict["config"])
if widget_dict["tooltip"] is not None:
@ -746,7 +771,7 @@ class ControlBuilder():
def build_control(self):
""" Build the correct control type for the option passed through """
logger.debug("Build config option control")
if self.option.control not in (ttk.Checkbutton, ttk.Radiobutton):
if self.option.control not in (ttk.Checkbutton, "radio", "multi"):
self.build_control_label()
self.build_one_control()
logger.debug("Built option control")
@ -764,10 +789,10 @@ class ControlBuilder():
def build_one_control(self):
""" Build and place the option controls """
logger.debug("Build control: '%s')", self.option.name)
if self.option.control == ttk.Scale:
if self.option.control == "scale":
ctl = self.slider_control()
elif self.option.control == ttk.Radiobutton:
ctl = self.radio_control()
elif self.option.control in ("radio", "multi"):
ctl = self._multi_option_control(self.option.control)
elif self.option.control == ttk.Checkbutton:
ctl = self.control_to_checkframe()
else:
@ -779,35 +804,65 @@ class ControlBuilder():
logger.debug("Built control: '%s'", self.option.name)
def radio_control(self):
""" Create a group of radio buttons """
logger.debug("Adding radio group: %s", self.option.name)
all_help = [line for line in self.option.helptext.splitlines()]
if any(line.startswith(" - ") for line in all_help):
intro = all_help[0]
helpitems = {re.sub(r'[^A-Za-z0-9\-]+', '',
line.split()[1].lower()): " ".join(line.split()[1:])
for line in all_help
if line.startswith(" - ")}
def _multi_option_control(self, option_type):
""" Create a group of buttons for single or multi-select
Parameters
----------
option_type: {"radio", "multi"}
The type of boxes that this control should hold. "radio" for single item select,
"multi" for multi item select.
"""
logger.debug("Adding %s group: %s", option_type, self.option.name)
help_intro, help_items = self._get_multi_help_items(self.option.helptext)
ctl = ttk.LabelFrame(self.frame,
text=self.option.title,
name="radio_labelframe")
radio_holder = AutoFillContainer(ctl, self.option_columns, self.option_columns)
name="{}_labelframe".format(option_type))
holder = AutoFillContainer(ctl, self.option_columns, self.option_columns)
for choice in self.option.choices:
radio = ttk.Radiobutton(radio_holder.subframe,
text=choice.replace("_", " ").title(),
value=choice,
variable=self.option.tk_var)
if choice.lower() in helpitems:
ctl = ttk.Radiobutton if option_type == "radio" else MultiOption
ctl = ctl(holder.subframe,
text=choice.replace("_", " ").title(),
value=choice,
variable=self.option.tk_var)
if choice.lower() in help_items:
self.helpset = True
helptext = helpitems[choice.lower()].capitalize()
helptext = help_items[choice.lower()].capitalize()
helptext = "{}\n\n - {}".format(
'. '.join(item.capitalize() for item in helptext.split('. ')),
intro)
_get_tooltip(radio, text=helptext, wraplength=600)
radio.pack(anchor=tk.W)
logger.debug("Added radio option %s", choice)
return radio_holder.parent
help_intro)
_get_tooltip(ctl, text=helptext, wraplength=600)
ctl.pack(anchor=tk.W)
logger.debug("Added %s option %s", option_type, choice)
return holder.parent
@staticmethod
def _get_multi_help_items(helptext):
""" Split the help text up, for formatted help text, into the individual options
for multi/radio buttons.
Parameters
----------
helptext: str
The raw help text for this cli. option
Returns
-------
tuple (`str`, `dict`)
The help text intro and a dictionary containing the help text split into separate
entries for each option choice
"""
logger.debug("raw help: %s", helptext)
all_help = helptext.splitlines()
intro = ""
if any(line.startswith(" - ") for line in all_help):
intro = all_help[0]
retval = (intro, {re.sub(r'[^A-Za-z0-9\-]+', '',
line.split()[1].lower()): " ".join(line.split()[1:])
for line in all_help if line.startswith(" - ")})
logger.debug("help items: %s", retval)
return retval
def slider_control(self):
""" A slider control with corresponding Entry box """
@ -829,7 +884,7 @@ class ControlBuilder():
d_type=self.option.dtype,
round_to=self.option.rounding,
min_max=self.option.min_max)
ctl = self.option.control(self.frame, variable=self.option.tk_var, command=cmd)
ctl = ttk.Scale(self.frame, variable=self.option.tk_var, command=cmd)
_add_command(ctl.cget("command"), cmd)
rc_menu = _get_contextmenu(tbox)
rc_menu.cm_bind()
@ -885,7 +940,7 @@ class ControlBuilder():
rc_menu.cm_bind()
if self.option.choices:
logger.debug("Adding combo choices: %s", self.option.choices)
ctl["values"] = [choice for choice in self.option.choices]
ctl["values"] = self.option.choices
ctl["state"] = "readonly"
logger.debug("Added control to Options Frame: %s", self.option.name)
return ctl

View File

@ -679,3 +679,84 @@ class Tooltip:
if topwidget:
topwidget.destroy()
self._topwidget = None
class MultiOption(ttk.Checkbutton): # pylint: disable=too-many-ancestors
""" Similar to the standard :class:`ttk.Radio` widget, but with the ability to select
multiple pre-defined options. Selected options are generated as `nargs` for the argument
parser to consume.
Parameters
----------
parent: :class:`ttk.Frame`
The tkinter parent widget for the check button
value: str
The raw option value for this check button
variable: :class:`tkinter.StingVar`
The master variable for the group of check buttons that this check button will belong to.
The output of this variable will be a string containing a space separated list of the
selected check button options
"""
def __init__(self, parent, value, variable, **kwargs):
self._tk_var = tk.BooleanVar()
self._tk_var.set(False)
super().__init__(parent, variable=self._tk_var, **kwargs)
self._value = value
self._master_variable = variable
self._tk_var.trace("w", self._on_update)
self._master_variable.trace("w", self._on_master_update)
@property
def _master_list(self):
""" list: The contents of the check box group's :attr:`_master_variable` in list form.
Selected check boxes will appear in this list. """
retval = self._master_variable.get().split()
logger.trace(retval)
return retval
@property
def _master_needs_update(self):
""" bool: ``True`` if :attr:`_master_variable` requires updating otherwise ``False``. """
active = self._tk_var.get()
retval = ((active and self._value not in self._master_list) or
(not active and self._value in self._master_list))
logger.trace(retval)
return retval
def _on_update(self, *args): # pylint: disable=unused-argument
""" Update the master variable on a check button change.
The value for this checked option is added or removed from the :attr:`_master_variable`
on a ``True``, ``False`` change for this check button.
Parameters
----------
args: tuple
Required for variable callback, but unused
"""
if not self._master_needs_update:
return
new_vals = self._master_list + [self._value] if self._tk_var.get() else [
val
for val in self._master_list
if val != self._value]
val = " ".join(new_vals)
logger.trace("Setting master variable to: %s", val)
self._master_variable.set(val)
def _on_master_update(self, *args): # pylint: disable=unused-argument
""" Update the check button on a master variable change (e.g. load .fsw file in the GUI).
The value for this option is set to ``True`` or ``False`` depending on it's existence in
the :attr:`_master_variable`
Parameters
----------
args: tuple
Required for variable callback, but unused
"""
if not self._master_needs_update:
return
state = self._value in self._master_list
logger.trace("Setting '%s' to %s", self._value, state)
self._tk_var.set(state)

View File

@ -9,7 +9,7 @@ import re
import sys
from collections import OrderedDict
from lib import cli
from lib.cli import actions, args as cli
from .utils import get_images
from .control_helper import ControlPanelOption
@ -121,7 +121,8 @@ class CliOptions():
group=opt.get("group", None),
default=opt.get("default", None),
choices=opt.get("choices", None),
is_radio=opt.get("action", "") == cli.Radio,
is_radio=opt.get("action", "") == actions.Radio,
is_multi_option=opt.get("action", "") == actions.MultiOption,
rounding=self.get_rounding(opt),
min_max=opt.get("min_max", None),
sysbrowser=self.get_sysbrowser(opt, command_options, command),
@ -167,13 +168,12 @@ class CliOptions():
def get_sysbrowser(self, option, options, command):
""" Return the system file browser and file types if required else None """
action = option.get("action", None)
if action not in (cli.FullPaths,
cli.DirFullPaths,
cli.FileFullPaths,
cli.FilesFullPaths,
cli.DirOrFileFullPaths,
cli.SaveFileFullPaths,
cli.ContextFullPaths):
if action not in (actions.DirFullPaths,
actions.FileFullPaths,
actions.FilesFullPaths,
actions.DirOrFileFullPaths,
actions.SaveFileFullPaths,
actions.ContextFullPaths):
return None
retval = dict()
@ -182,15 +182,15 @@ class CliOptions():
self.expand_action_option(option, options)
action_option = option["action_option"]
retval["filetypes"] = option.get("filetypes", "default")
if action == cli.FileFullPaths:
if action == actions.FileFullPaths:
retval["browser"] = ["load"]
elif action == cli.FilesFullPaths:
elif action == actions.FilesFullPaths:
retval["browser"] = ["multi_load"]
elif action == cli.SaveFileFullPaths:
elif action == actions.SaveFileFullPaths:
retval["browser"] = ["save"]
elif action == cli.DirOrFileFullPaths:
elif action == actions.DirOrFileFullPaths:
retval["browser"] = ["folder", "load"]
elif action == cli.ContextFullPaths and action_option:
elif action == actions.ContextFullPaths and action_option:
retval["browser"] = ["context"]
retval["command"] = command
retval["action_option"] = action_option

View File

@ -43,8 +43,9 @@ class Extractor():
The name of a detector plugin as exists in :mod:`plugins.extract.detect`
aligner: str
The name of an aligner plugin as exists in :mod:`plugins.extract.align`
masker: str
The name of a masker plugin as exists in :mod:`plugins.extract.mask`
masker: str or list
The name of a masker plugin(s) as exists in :mod:`plugins.extract.mask`.
This can be a single masker or a list of multiple maskers
configfile: str, optional
The path to a custom ``extract.ini`` configfile. If ``None`` then the system
:file:`config/extract.ini` file will be used.
@ -65,7 +66,7 @@ class Extractor():
images fed to the aligner.Default: ``None``
image_is_aligned: bool, optional
Used to set the :attr:`plugins.extract.mask.image_is_aligned` attribute. Indicates to the
masker that the fed in image is an aligned face rather than a frame.Default: ``False``
masker that the fed in image is an aligned face rather than a frame. Default: ``False``
Attributes
----------

View File

@ -149,7 +149,7 @@ class Convert(): # pylint:disable=too-few-public-methods
def process(self):
""" The entry point for triggering the Conversion Process.
Should only be called from :class:`lib.cli.ScriptExecutor`
Should only be called from :class:`lib.cli.launcher.ScriptExecutor`
"""
logger.debug("Starting Conversion")
# queue_manager.debug_monitor(5)

View File

@ -38,7 +38,6 @@ class Extract(): # pylint:disable=too-few-public-methods
def __init__(self, arguments):
logger.debug("Initializing %s: (args: %s", self.__class__.__name__, arguments)
self._args = arguments
self._output_dir = str(get_folder(self._args.output_dir))
logger.info("Output Directory: %s", self._args.output_dir)
@ -51,9 +50,12 @@ class Extract(): # pylint:disable=too-few-public-methods
self._post_process = PostProcess(arguments)
configfile = self._args.configfile if hasattr(self._args, "configfile") else None
normalization = None if self._args.normalization == "none" else self._args.normalization
maskers = ["components", "extended"]
maskers += self._args.masker if self._args.masker else []
self._extractor = Extractor(self._args.detector,
self._args.aligner,
[self._args.masker, "components", "extended"],
maskers,
configfile=configfile,
multiprocess=not self._args.singleprocess,
rotate_images=self._args.rotate_images,
@ -106,7 +108,7 @@ class Extract(): # pylint:disable=too-few-public-methods
def process(self):
""" The entry point for triggering the Extraction Process.
Should only be called from :class:`lib.cli.ScriptExecutor`
Should only be called from :class:`lib.cli.launcher.ScriptExecutor`
"""
logger.info('Starting, this may take a while...')
# from lib.queue_manager import queue_manager ; queue_manager.debug_monitor(3)

View File

@ -139,7 +139,7 @@ class Train():
def process(self):
""" The entry point for triggering the Training Process.
Should only be called from :class:`lib.cli.ScriptExecutor`
Should only be called from :class:`lib.cli.launcher.ScriptExecutor`
"""
logger.debug("Starting Training Process")
logger.info("Training data directory: %s", self._args.model_dir)

View File

@ -6,7 +6,7 @@ import sys
from importlib import import_module
# Importing the various tools
from lib.cli import FullHelpArgumentParser
from lib.cli.args import FullHelpArgumentParser
# Python version check
if sys.version_info[0] < 3:

View File

@ -1,5 +1,5 @@
#!/usr/bin/env python3
""" Tools for manipulating the alignments seralized file """
""" Tools for manipulating the alignments serialized file """
import sys
import logging

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python3
""" Command Line Arguments for tools """
from lib.cli import FaceSwapArgs
from lib.cli import DirOrFileFullPaths, DirFullPaths, FilesFullPaths, Radio, Slider
from lib.cli.args import FaceSwapArgs
from lib.cli.actions import DirOrFileFullPaths, DirFullPaths, FilesFullPaths, Radio, Slider
_HELPTEXT = "This command lets you perform various tasks pertaining to an alignments file."

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python3
""" Command Line Arguments for tools """
from lib.cli import FaceSwapArgs
from lib.cli import ContextFullPaths, FileFullPaths, Radio
from lib.cli.args import FaceSwapArgs
from lib.cli.actions import ContextFullPaths, FileFullPaths, Radio
from lib.utils import _image_extensions
_HELPTEXT = "This command allows you to easily execute common ffmpeg tasks."

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python3
""" Command Line Arguments for tools """
from lib.cli import FaceSwapArgs
from lib.cli import (DirOrFileFullPaths, DirFullPaths, FileFullPaths, Radio, Slider)
from lib.cli.args import FaceSwapArgs
from lib.cli.actions import (DirOrFileFullPaths, DirFullPaths, FileFullPaths, Radio, Slider)
from plugins.plugin_loader import PluginLoader
_HELPTEXT = "This command lets you generate masks for existing alignments."

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python3
""" Command Line Arguments for tools """
from lib.cli import FaceSwapArgs
from lib.cli import DirOrFileFullPaths, DirFullPaths, FileFullPaths
from lib.cli.args import FaceSwapArgs
from lib.cli.actions import DirOrFileFullPaths, DirFullPaths, FileFullPaths
_HELPTEXT = "This command allows you to preview swaps to tweak convert settings."

View File

@ -16,7 +16,7 @@ import numpy as np
from PIL import Image, ImageTk
from lib.aligner import Extract as AlignerExtract
from lib.cli import ConvertArgs
from lib.cli.args import ConvertArgs
from lib.gui.utils import get_images, get_config, initialize_config, initialize_images
from lib.gui.custom_widgets import Tooltip
from lib.gui.control_helper import ControlPanel, ControlPanelOption

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python3
""" Command Line Arguments for tools """
from lib.cli import FaceSwapArgs
from lib.cli import DirFullPaths
from lib.cli.args import FaceSwapArgs
from lib.cli.actions import DirFullPaths
_HELPTEXT = "This command lets you restore models from backup."

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python3
""" Command Line Arguments for tools """
from lib.cli import FaceSwapArgs
from lib.cli import DirFullPaths, SaveFileFullPaths, Radio, Slider
from lib.cli.args import FaceSwapArgs
from lib.cli.actions import DirFullPaths, SaveFileFullPaths, Radio, Slider
_HELPTEXT = "This command lets you sort images using various methods."