mirror of
https://github.com/deepfakes/faceswap
synced 2025-06-07 19:05:02 -04:00
* Pre push commit. Add filetypes support to gui through new classes in lib/cli.py Add various new functions to tools/effmpeg.py * Finish developing basic effmpeg functionality. Ready for public alpha test. * Add ffmpy to requirements. Fix gen-vid to allow specifying a new file in GUI. Fix extract throwing an error when supplied with a valid directory. Add two new gui user pop interactions: save (allows you to create new files/directories) and nothing (disables the prompt button when it's not needed). Improve logic and argument processing in effmpeg. * Fix post merge bugs. Reformat tools.py to match the new style of faceswap.py Fix some whitespace issues. * Fix matplotlib.use() being called after pyplot was imported. * Fix various effmpeg bugs and add ability do terminate nested subprocess to GUI. effmpeg changes: Fix get-fps not printing to terminal. Fix mux-audio not working. Add verbosity option. If verbose is not specified than ffmpeg output is reduced with the -hide_banner flag. scripts/gui.py changes: Add ability to terminate nested subprocesses, i.e. the following type of process tree should now be terminated safely: gui -> command -> command-subprocess -> command-subprocess -> command-sub-subprocess * Add functionality to tools/effmpeg.py, fix some docstring and print statement issues in some files. tools/effmpeg.py: Transpose choices now display detailed name in GUI, while in cli they can still be entered as a number or the full command name. Add quiet option to effmpeg that only shows critical ffmpeg errors. Improve user input handling. lib/cli.py; scripts/convert.py; scripts/extract.py; scripts/train.py: Fix some line length issues and typos in docstrings, help text and print statements. Fix some whitespace issues. lib/cli.py: Add filetypes to '--alignments' argument. Change argument action to DirFullPaths where appropriate. * Bug fixes and improvements to tools/effmpeg.py Fix bug where duration would not be used even when end time was not set. Add option to specify output filetype for extraction. Enchance gen-vid to be able to generate a video from images that were zero padded to any arbitrary number, and not just 5. Enchance gen-vid to be able to use any of the image formats that a video can be extracted into. Improve gen-vid output video quality. Minor code quality improvements and ffmpeg argument formatting improvements. * Remove dependency on psutil in scripts/gui.py and various small improvements. lib/utils.py: Add _image_extensions and _video_extensions as global variables to make them easily portable across all of faceswap. Fix lack of new lines between function and class declarions to conform to PEP8. Fix some typos and line length issues in doctsrings and comments. scripts/convert.py: Make tqdm print to stdout. scripts/extract.py: Make tqdm print to stdout. Apply workaround for occasional TqdmSynchronisationWarning being thrown. Fix some typos and line length issues in doctsrings and comments. scripts/fsmedia.py: Did TODO in scripts/fsmedia.py in Faces.load_extractor(): TODO Pass extractor_name as argument Fix lack of new lines between function and class declarions to conform to PEP8. Fix some typos and line length issues in doctsrings and comments. Change 2 print statements to use format() for string formatting instead of the old '%'. scripts/gui.py: Refactor subprocess generation and termination to remove dependency on psutil. Fix some typos and line length issues in comments. tools/effmpeg.py Refactor DataItem class to use new lib/utils.py global media file extensions. Improve ffmpeg subprocess termination handling.
1279 lines
46 KiB
Python
1279 lines
46 KiB
Python
#!/usr/bin python3
|
|
""" The optional GUI for faceswap """
|
|
|
|
import os
|
|
import re
|
|
import signal
|
|
import subprocess
|
|
from subprocess import PIPE, Popen, TimeoutExpired
|
|
import sys
|
|
|
|
from argparse import SUPPRESS
|
|
from math import ceil, floor
|
|
from threading import Thread
|
|
from time import time
|
|
|
|
import matplotlib
|
|
matplotlib.use('TkAgg')
|
|
import matplotlib.animation as animation
|
|
from matplotlib import pyplot as plt
|
|
from matplotlib import style
|
|
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
|
|
|
import numpy
|
|
|
|
from lib.cli import FullPaths, ComboFullPaths, DirFullPaths, FileFullPaths
|
|
from lib.Serializer import JSONSerializer
|
|
|
|
|
|
PATHSCRIPT = os.path.realpath(os.path.dirname(sys.argv[0]))
|
|
|
|
# An error will be thrown when importing tkinter for users without tkinter
|
|
# distribution packages or without an X-Console. Therefore if importing fails
|
|
# no attempt will be made to instantiate the gui.
|
|
try:
|
|
import tkinter as tk
|
|
from tkinter import ttk
|
|
from tkinter import filedialog
|
|
from tkinter import messagebox
|
|
from tkinter import TclError
|
|
except ImportError:
|
|
tk = None
|
|
ttk = None
|
|
filedialog = None
|
|
messagebox = None
|
|
TclError = None
|
|
|
|
|
|
class Utils(object):
|
|
""" Inter-class object holding items that are required across classes """
|
|
|
|
def __init__(self, options, calling_file="faceswap.py"):
|
|
self.opts = options
|
|
|
|
self.icons = dict()
|
|
self.guitext = dict()
|
|
self.actionbtns = dict()
|
|
|
|
self.console = None
|
|
self.debugconsole = False
|
|
|
|
self.serializer = JSONSerializer
|
|
self.filetypes = (('Faceswap files', '*.fsw'), ('All files', '*.*'))
|
|
|
|
self.task = FaceswapControl(self, calling_file=calling_file)
|
|
self.runningtask = False
|
|
|
|
self.previewloc = os.path.join(PATHSCRIPT, '.gui_preview.png')
|
|
|
|
self.lossdict = dict()
|
|
|
|
def init_tk(self):
|
|
""" TK System must be on prior to setting tk variables,
|
|
so initialised from GUI """
|
|
pathicons = os.path.join(PATHSCRIPT, 'icons')
|
|
self.icons['folder'] = tk.PhotoImage(
|
|
file=os.path.join(pathicons, 'open_folder.png'))
|
|
self.icons['load'] = tk.PhotoImage(
|
|
file=os.path.join(pathicons, 'open_file.png'))
|
|
self.icons['save'] = tk.PhotoImage(
|
|
file=os.path.join(pathicons, 'save.png'))
|
|
self.icons['reset'] = tk.PhotoImage(
|
|
file=os.path.join(pathicons, 'reset.png'))
|
|
self.icons['clear'] = tk.PhotoImage(
|
|
file=os.path.join(pathicons, 'clear.png'))
|
|
|
|
self.guitext['help'] = tk.StringVar()
|
|
self.guitext['status'] = tk.StringVar()
|
|
|
|
def action_command(self, command):
|
|
""" The action to perform when the action button is pressed """
|
|
if self.runningtask:
|
|
self.action_terminate()
|
|
else:
|
|
self.action_execute(command)
|
|
|
|
def action_execute(self, command):
|
|
""" Execute the task in Faceswap.py """
|
|
self.clear_console()
|
|
self.task.prepare(self.opts, command)
|
|
self.task.execute_script()
|
|
|
|
def action_terminate(self):
|
|
""" Terminate the subprocess Faceswap.py task """
|
|
self.task.terminate()
|
|
self.runningtask = False
|
|
self.clear_display_panel()
|
|
self.change_action_button()
|
|
|
|
def clear_display_panel(self):
|
|
""" Clear the preview window and graph """
|
|
self.delete_preview()
|
|
self.lossdict = dict()
|
|
|
|
def change_action_button(self):
|
|
""" Change the action button to relevant control """
|
|
for cmd in self.actionbtns.keys():
|
|
btnact = self.actionbtns[cmd]
|
|
if self.runningtask:
|
|
ttl = 'Terminate'
|
|
hlp = 'Exit the running process'
|
|
else:
|
|
ttl = cmd.title()
|
|
hlp = 'Run the {} script'.format(cmd.title())
|
|
btnact.config(text=ttl)
|
|
Tooltip(btnact, text=hlp, wraplength=200)
|
|
|
|
def clear_console(self):
|
|
""" Clear the console output screen """
|
|
self.console.delete(1.0, tk.END)
|
|
|
|
def load_config(self, command=None):
|
|
""" Load a saved config file """
|
|
cfgfile = filedialog.askopenfile(mode='r', filetypes=self.filetypes)
|
|
if not cfgfile:
|
|
return
|
|
cfg = self.serializer.unmarshal(cfgfile.read())
|
|
if command is None:
|
|
for cmd, opts in cfg.items():
|
|
self.set_command_args(cmd, opts)
|
|
else:
|
|
opts = cfg.get(command, None)
|
|
if opts:
|
|
self.set_command_args(command, opts)
|
|
else:
|
|
self.clear_console()
|
|
print('No ' + command + ' section found in file')
|
|
|
|
def set_command_args(self, command, options):
|
|
""" Pass the saved config items back to the GUI """
|
|
for srcopt, srcval in options.items():
|
|
for dstopts in self.opts[command]:
|
|
if dstopts['control_title'] == srcopt:
|
|
dstopts['value'].set(srcval)
|
|
break
|
|
|
|
def save_config(self, command=None):
|
|
""" Save the current GUI state to a config file in json format """
|
|
cfgfile = filedialog.asksaveasfile(mode='w',
|
|
filetypes=self.filetypes,
|
|
defaultextension='.fsw')
|
|
if not cfgfile:
|
|
return
|
|
if command is None:
|
|
cfg = {cmd: {opt['control_title']: opt['value'].get() for opt in opts}
|
|
for cmd, opts in self.opts.items()}
|
|
else:
|
|
cfg = {command: {opt['control_title']: opt['value'].get()
|
|
for opt in self.opts[command]}}
|
|
cfgfile.write(self.serializer.marshal(cfg))
|
|
cfgfile.close()
|
|
|
|
def reset_config(self, command=None):
|
|
""" Reset the GUI to the default values """
|
|
if command is None:
|
|
options = [opt for opts in self.opts.values() for opt in opts]
|
|
else:
|
|
options = [opt for opt in self.opts[command]]
|
|
for option in options:
|
|
default = option.get('default', '')
|
|
default = '' if default is None else default
|
|
option['value'].set(default)
|
|
|
|
def clear_config(self, command=None):
|
|
""" Clear all values from the GUI """
|
|
if command is None:
|
|
options = [opt for opts in self.opts.values() for opt in opts]
|
|
else:
|
|
options = [opt for opt in self.opts[command]]
|
|
for option in options:
|
|
if isinstance(option['value'].get(), bool):
|
|
option['value'].set(False)
|
|
elif isinstance(option['value'].get(), int):
|
|
option['value'].set(0)
|
|
else:
|
|
option['value'].set('')
|
|
|
|
def delete_preview(self):
|
|
""" Delete the preview file """
|
|
if os.path.exists(self.previewloc):
|
|
os.remove(self.previewloc)
|
|
|
|
def get_chosen_action(self, task_name):
|
|
return self.opts[task_name][0]['value'].get()
|
|
|
|
|
|
class Tooltip:
|
|
"""
|
|
Create a tooltip for a given widget as the mouse goes on it.
|
|
|
|
Adapted from StackOverflow:
|
|
|
|
http://stackoverflow.com/questions/3221956/
|
|
what-is-the-simplest-way-to-make-tooltips-
|
|
in-tkinter/36221216#36221216
|
|
|
|
http://www.daniweb.com/programming/software-development/
|
|
code/484591/a-tooltip-class-for-tkinter
|
|
|
|
- Originally written by vegaseat on 2014.09.09.
|
|
|
|
- Modified to include a delay time by Victor Zaccardo on 2016.03.25.
|
|
|
|
- Modified
|
|
- to correct extreme right and extreme bottom behavior,
|
|
- to stay inside the screen whenever the tooltip might go out on
|
|
the top but still the screen is higher than the tooltip,
|
|
- to use the more flexible mouse positioning,
|
|
- to add customizable background color, padding, waittime and
|
|
wraplength on creation
|
|
by Alberto Vassena on 2016.11.05.
|
|
|
|
Tested on Ubuntu 16.04/16.10, running Python 3.5.2
|
|
|
|
"""
|
|
|
|
def __init__(self, widget,
|
|
*,
|
|
background='#FFFFEA',
|
|
pad=(5, 3, 5, 3),
|
|
text='widget info',
|
|
waittime=400,
|
|
wraplength=250):
|
|
|
|
self.waittime = waittime # in miliseconds, originally 500
|
|
self.wraplength = wraplength # in pixels, originally 180
|
|
self.widget = widget
|
|
self.text = text
|
|
self.widget.bind("<Enter>", self.on_enter)
|
|
self.widget.bind("<Leave>", self.on_leave)
|
|
self.widget.bind("<ButtonPress>", self.on_leave)
|
|
self.background = background
|
|
self.pad = pad
|
|
self.ident = None
|
|
self.topwidget = None
|
|
|
|
def on_enter(self, event=None):
|
|
""" Schedule on an enter event """
|
|
self.schedule()
|
|
|
|
def on_leave(self, event=None):
|
|
""" Unschedule on a leave event """
|
|
self.unschedule()
|
|
self.hide()
|
|
|
|
def schedule(self):
|
|
""" Show the tooltip after wait period """
|
|
self.unschedule()
|
|
self.ident = self.widget.after(self.waittime, self.show)
|
|
|
|
def unschedule(self):
|
|
""" Hide the tooltip """
|
|
id_ = self.ident
|
|
self.ident = None
|
|
if id_:
|
|
self.widget.after_cancel(id_)
|
|
|
|
def show(self):
|
|
""" Show the tooltip """
|
|
def tip_pos_calculator(widget, label,
|
|
*,
|
|
tip_delta=(10, 5), pad=(5, 3, 5, 3)):
|
|
""" Calculate the tooltip position """
|
|
|
|
s_width, s_height = widget.winfo_screenwidth(), widget.winfo_screenheight()
|
|
|
|
width, height = (pad[0] + label.winfo_reqwidth() + pad[2],
|
|
pad[1] + label.winfo_reqheight() + pad[3])
|
|
|
|
mouse_x, mouse_y = widget.winfo_pointerxy()
|
|
|
|
x_1, y_1 = mouse_x + tip_delta[0], mouse_y + tip_delta[1]
|
|
x_2, y_2 = x_1 + width, y_1 + height
|
|
|
|
x_delta = x_2 - s_width
|
|
if x_delta < 0:
|
|
x_delta = 0
|
|
y_delta = y_2 - s_height
|
|
if y_delta < 0:
|
|
y_delta = 0
|
|
|
|
offscreen = (x_delta, y_delta) != (0, 0)
|
|
|
|
if offscreen:
|
|
|
|
if x_delta:
|
|
x_1 = mouse_x - tip_delta[0] - width
|
|
|
|
if y_delta:
|
|
y_1 = mouse_y - tip_delta[1] - height
|
|
|
|
offscreen_again = y_1 < 0 # out on the top
|
|
|
|
if offscreen_again:
|
|
# No further checks will be done.
|
|
|
|
# TIP:
|
|
# A further mod might automagically augment the
|
|
# wraplength when the tooltip is too high to be
|
|
# kept inside the screen.
|
|
y_1 = 0
|
|
|
|
return x_1, y_1
|
|
|
|
background = self.background
|
|
pad = self.pad
|
|
widget = self.widget
|
|
|
|
# creates a toplevel window
|
|
self.topwidget = tk.Toplevel(widget)
|
|
|
|
# Leaves only the label and removes the app window
|
|
self.topwidget.wm_overrideredirect(True)
|
|
|
|
win = tk.Frame(self.topwidget,
|
|
background=background,
|
|
borderwidth=0)
|
|
label = tk.Label(win,
|
|
text=self.text,
|
|
justify=tk.LEFT,
|
|
background=background,
|
|
relief=tk.SOLID,
|
|
borderwidth=0,
|
|
wraplength=self.wraplength)
|
|
|
|
label.grid(padx=(pad[0], pad[2]),
|
|
pady=(pad[1], pad[3]),
|
|
sticky=tk.NSEW)
|
|
win.grid()
|
|
|
|
xpos, ypos = tip_pos_calculator(widget, label)
|
|
|
|
self.topwidget.wm_geometry("+%d+%d" % (xpos, ypos))
|
|
|
|
def hide(self):
|
|
""" Hide the tooltip """
|
|
topwidget = self.topwidget
|
|
if topwidget:
|
|
topwidget.destroy()
|
|
self.topwidget = None
|
|
|
|
|
|
class FaceswapGui(object):
|
|
""" The Graphical User Interface """
|
|
|
|
def __init__(self, utils, calling_file='faceswap.py'):
|
|
self.gui = tk.Tk()
|
|
self.utils = utils
|
|
self.calling_file = calling_file
|
|
self.utils.delete_preview()
|
|
self.utils.init_tk()
|
|
self.gui.protocol('WM_DELETE_WINDOW', self.close_app)
|
|
|
|
def build_gui(self):
|
|
""" Build the GUI """
|
|
self.gui.title(self.calling_file)
|
|
self.menu()
|
|
|
|
container = tk.PanedWindow(self.gui,
|
|
sashrelief=tk.RAISED,
|
|
orient=tk.VERTICAL)
|
|
container.pack(fill=tk.BOTH, expand=True)
|
|
|
|
topcontainer = tk.PanedWindow(container,
|
|
sashrelief=tk.RAISED,
|
|
orient=tk.HORIZONTAL)
|
|
container.add(topcontainer)
|
|
|
|
bottomcontainer = ttk.Frame(container, height=150)
|
|
container.add(bottomcontainer)
|
|
|
|
optsnotebook = ttk.Notebook(topcontainer, width=400, height=500)
|
|
topcontainer.add(optsnotebook)
|
|
|
|
if self.calling_file == 'faceswap.py':
|
|
# Commands explicitly stated to ensure consistent ordering
|
|
cmdlist = ('extract', 'train', 'convert')
|
|
else:
|
|
cmdlist = self.utils.opts.keys()
|
|
|
|
for command in cmdlist:
|
|
commandtab = CommandTab(self.utils, optsnotebook, command)
|
|
commandtab.build_tab()
|
|
|
|
dspnotebook = ttk.Notebook(topcontainer, width=780)
|
|
topcontainer.add(dspnotebook)
|
|
|
|
for display in ('graph', 'preview'):
|
|
displaytab = DisplayTab(self.utils, dspnotebook, display)
|
|
displaytab.build_tab()
|
|
|
|
self.add_console(bottomcontainer)
|
|
self.add_status_bar(bottomcontainer)
|
|
|
|
def menu(self):
|
|
""" Menu bar for loading and saving configs """
|
|
menubar = tk.Menu(self.gui)
|
|
filemenu = tk.Menu(menubar, tearoff=0)
|
|
filemenu.add_command(label='Load full config...',
|
|
command=self.utils.load_config)
|
|
filemenu.add_command(label='Save full config...',
|
|
command=self.utils.save_config)
|
|
filemenu.add_separator()
|
|
filemenu.add_command(label='Reset all to default',
|
|
command=self.utils.reset_config)
|
|
filemenu.add_command(label='Clear all',
|
|
command=self.utils.clear_config)
|
|
filemenu.add_separator()
|
|
filemenu.add_command(label='Quit', command=self.close_app)
|
|
menubar.add_cascade(label="File", menu=filemenu)
|
|
self.gui.config(menu=menubar)
|
|
|
|
def add_console(self, frame):
|
|
""" Build the output console """
|
|
consoleframe = ttk.Frame(frame)
|
|
consoleframe.pack(side=tk.TOP, anchor=tk.W, padx=10, pady=(2, 0),
|
|
fill=tk.BOTH, expand=True)
|
|
console = ConsoleOut(consoleframe, self.utils)
|
|
console.build_console()
|
|
|
|
def add_status_bar(self, frame):
|
|
""" Build the info text section page """
|
|
statusframe = ttk.Frame(frame)
|
|
statusframe.pack(side=tk.BOTTOM, anchor=tk.W, padx=10, pady=2,
|
|
fill=tk.X, expand=False)
|
|
|
|
lbltitle = ttk.Label(statusframe, text='Status:', width=6, anchor=tk.W)
|
|
lbltitle.pack(side=tk.LEFT, expand=False)
|
|
self.utils.guitext['status'].set('Ready')
|
|
lblstatus = ttk.Label(statusframe,
|
|
width=20,
|
|
textvariable=self.utils.guitext['status'],
|
|
anchor=tk.W)
|
|
lblstatus.pack(side=tk.LEFT, anchor=tk.W, fill=tk.X, expand=True)
|
|
|
|
def close_app(self):
|
|
""" Close Python. This is here because the graph animation function
|
|
continues to
|
|
run even when tkinter has gone away """
|
|
confirm = messagebox.askokcancel
|
|
confirmtxt = 'Processes are still running. Are you sure...?'
|
|
if self.utils.runningtask and not confirm('Close', confirmtxt):
|
|
return
|
|
if self.utils.runningtask:
|
|
self.utils.task.terminate()
|
|
self.utils.delete_preview()
|
|
self.gui.quit()
|
|
exit()
|
|
|
|
|
|
class ConsoleOut(object):
|
|
""" The Console out tab of the Display section """
|
|
|
|
def __init__(self, frame, utils):
|
|
self.frame = frame
|
|
utils.console = tk.Text(self.frame)
|
|
self.console = utils.console
|
|
self.debug = utils.debugconsole
|
|
|
|
def build_console(self):
|
|
""" Build and place the console """
|
|
self.console.config(width=100, height=6, bg='gray90', fg='black')
|
|
self.console.pack(side=tk.LEFT, anchor=tk.N, fill=tk.BOTH, expand=True)
|
|
|
|
scrollbar = ttk.Scrollbar(self.frame, command=self.console.yview)
|
|
scrollbar.pack(side=tk.LEFT, fill='y')
|
|
self.console.configure(yscrollcommand=scrollbar.set)
|
|
|
|
if self.debug:
|
|
print('Console debug activated. Outputting to main terminal')
|
|
else:
|
|
sys.stdout = SysOutRouter(console=self.console, out_type="stdout")
|
|
sys.stderr = SysOutRouter(console=self.console, out_type="stderr")
|
|
|
|
|
|
class SysOutRouter(object):
|
|
""" Route stdout/stderr to the console window """
|
|
|
|
def __init__(self, console=None, out_type=None):
|
|
self.console = console
|
|
self.out_type = out_type
|
|
self.color = ("black" if out_type == "stdout" else "red")
|
|
|
|
def write(self, string):
|
|
""" Capture stdout/stderr """
|
|
self.console.insert(tk.END, string, self.out_type)
|
|
self.console.tag_config(self.out_type, foreground=self.color)
|
|
self.console.see(tk.END)
|
|
|
|
@staticmethod
|
|
def flush():
|
|
""" If flush is forced, send it to normal terminal """
|
|
sys.__stdout__.flush()
|
|
|
|
|
|
class CommandTab(object):
|
|
""" Tabs to hold the command options """
|
|
|
|
def __init__(self, utils, notebook, command):
|
|
self.utils = utils
|
|
self.notebook = notebook
|
|
self.page = ttk.Frame(self.notebook)
|
|
self.command = command
|
|
self.title = command.title()
|
|
|
|
def build_tab(self):
|
|
""" Build the tab """
|
|
actionframe = ActionFrame(self.utils, self.page, self.command)
|
|
actionframe.build_frame()
|
|
|
|
self.add_frame_separator()
|
|
opts_frame = OptionsFrame(self.utils, self.page, self.command)
|
|
opts_frame.build_frame()
|
|
|
|
self.notebook.add(self.page, text=self.title)
|
|
|
|
def add_frame_separator(self):
|
|
""" Add a separator between left and right frames """
|
|
sep = ttk.Frame(self.page, height=2, relief=tk.RIDGE)
|
|
sep.pack(fill=tk.X, pady=(5, 0), side=tk.BOTTOM)
|
|
|
|
|
|
class OptionsFrame(object):
|
|
""" Options Frame - Holds the Options for each command """
|
|
|
|
def __init__(self, utils, page, command):
|
|
self.utils = utils
|
|
self.page = page
|
|
self.command = command
|
|
|
|
self.canvas = tk.Canvas(self.page, bd=0, highlightthickness=0)
|
|
self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
|
|
self.optsframe = tk.Frame(self.canvas)
|
|
self.optscanvas = self.canvas.create_window((0, 0), window=self.optsframe, anchor=tk.NW)
|
|
|
|
def build_frame(self):
|
|
""" Build the options frame for this command """
|
|
self.add_scrollbar()
|
|
self.canvas.bind('<Configure>', self.resize_frame)
|
|
|
|
for option in self.utils.opts[self.command]:
|
|
optioncontrol = OptionControl(self.utils, option, self.optsframe)
|
|
optioncontrol.build_full_control()
|
|
|
|
def add_scrollbar(self):
|
|
""" Add a scrollbar to the options frame """
|
|
scrollbar = ttk.Scrollbar(self.page, command=self.canvas.yview)
|
|
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
|
self.canvas.config(yscrollcommand=scrollbar.set)
|
|
self.optsframe.bind("<Configure>", self.update_scrollbar)
|
|
|
|
def update_scrollbar(self, event):
|
|
""" Update the options frame scrollbar """
|
|
self.canvas.configure(scrollregion=self.canvas.bbox('all'))
|
|
|
|
def resize_frame(self, event):
|
|
""" Resize the options frame to fit the canvas """
|
|
canvas_width = event.width
|
|
self.canvas.itemconfig(self.optscanvas, width=canvas_width)
|
|
|
|
|
|
class OptionControl(object):
|
|
""" Build the correct control for the option parsed and place it on the
|
|
frame """
|
|
|
|
def __init__(self, utils, option, option_frame):
|
|
self.utils = utils
|
|
self.option = option
|
|
self.option_frame = option_frame
|
|
|
|
def build_full_control(self):
|
|
""" Build the correct control type for the option passed through """
|
|
ctl = self.option['control']
|
|
ctltitle = self.option['control_title']
|
|
sysbrowser = self.option['filesystem_browser']
|
|
ctlhelp = ' '.join(self.option.get('help', '').split())
|
|
ctlhelp = '. '.join(i.capitalize() for i in ctlhelp.split('. '))
|
|
ctlhelp = ctltitle + ' - ' + ctlhelp
|
|
ctlframe = self.build_one_control_frame()
|
|
dflt = self.option.get('default', '')
|
|
dflt = self.option.get('default', False) if ctl == ttk.Checkbutton else dflt
|
|
choices = self.option['choices'] if ctl == ttk.Combobox else None
|
|
|
|
self.build_one_control_label(ctlframe, ctltitle)
|
|
self.option['value'] = self.build_one_control(ctlframe,
|
|
ctl,
|
|
dflt,
|
|
ctlhelp,
|
|
choices,
|
|
sysbrowser)
|
|
|
|
def build_one_control_frame(self):
|
|
""" Build the frame to hold the control """
|
|
frame = ttk.Frame(self.option_frame)
|
|
frame.pack(fill=tk.X, expand=True)
|
|
return frame
|
|
|
|
@staticmethod
|
|
def build_one_control_label(frame, control_title):
|
|
""" Build and place the control label """
|
|
lbl = ttk.Label(frame, text=control_title, width=18, anchor=tk.W)
|
|
lbl.pack(padx=5, pady=5, side=tk.LEFT, anchor=tk.N)
|
|
|
|
def build_one_control(self, frame, control, default, helptext, choices,
|
|
sysbrowser):
|
|
""" Build and place the option controls """
|
|
default = default if default is not None else ''
|
|
|
|
var = tk.BooleanVar(
|
|
frame) if control == ttk.Checkbutton else tk.StringVar(frame)
|
|
var.set(default)
|
|
|
|
if sysbrowser is not None:
|
|
# if sysbrowser in "load file":
|
|
self.add_browser_buttons(frame, sysbrowser, var)
|
|
# elif sysbrowser == "combo":
|
|
# self.add_browser_combo_button(frame, sysbrowser, var)
|
|
|
|
ctlkwargs = {'variable': var} if control == ttk.Checkbutton else {
|
|
'textvariable': var}
|
|
packkwargs = {'anchor': tk.W} if control == ttk.Checkbutton else {
|
|
'fill': tk.X, 'expand': True}
|
|
ctl = control(frame, **ctlkwargs)
|
|
|
|
if control == ttk.Combobox:
|
|
ctl['values'] = [choice for choice in choices]
|
|
|
|
ctl.pack(padx=5, pady=5, **packkwargs)
|
|
Tooltip(ctl, text=helptext, wraplength=200)
|
|
return var
|
|
|
|
def add_browser_buttons(self, frame, sysbrowser, filepath):
|
|
""" Add correct file browser button for control """
|
|
if sysbrowser == "combo":
|
|
img = self.utils.icons['load']
|
|
else:
|
|
img = self.utils.icons[sysbrowser]
|
|
action = getattr(self, 'ask_' + sysbrowser)
|
|
filetypes = self.option['filetypes']
|
|
fileopn = ttk.Button(frame, image=img,
|
|
command=lambda cmd=action: cmd(filepath,
|
|
filetypes))
|
|
fileopn.pack(padx=(0, 5), side=tk.RIGHT)
|
|
|
|
@staticmethod
|
|
def ask_folder(filepath, filetypes=None):
|
|
"""
|
|
Pop-up to get path to a directory
|
|
:param filepath: tkinter StringVar object that will store the path to a
|
|
directory.
|
|
:param filetypes: Unused argument to allow filetypes to be given in
|
|
ask_load().
|
|
"""
|
|
dirname = filedialog.askdirectory()
|
|
if dirname:
|
|
filepath.set(dirname)
|
|
|
|
@staticmethod
|
|
def ask_load(filepath, filetypes=None):
|
|
""" Pop-up to get path to a file """
|
|
if filetypes is None:
|
|
filename = filedialog.askopenfilename()
|
|
else:
|
|
# In case filetypes were not configured properly in the
|
|
# arguments_list
|
|
try:
|
|
filename = filedialog.askopenfilename(filetypes=filetypes)
|
|
except TclError as te1:
|
|
filetypes = FileFullPaths.prep_filetypes(filetypes)
|
|
filename = filedialog.askopenfilename(filetypes=filetypes)
|
|
except TclError as te2:
|
|
filename = filedialog.askopenfilename()
|
|
if filename:
|
|
filepath.set(filename)
|
|
|
|
@staticmethod
|
|
def ask_save(filepath, filetypes=None):
|
|
""" Pop-up to get path to save a new file """
|
|
if filetypes is None:
|
|
filename = filedialog.asksaveasfilename()
|
|
else:
|
|
# In case filetypes were not configured properly in the
|
|
# arguments_list
|
|
try:
|
|
filename = filedialog.asksaveasfilename(filetypes=filetypes)
|
|
except TclError as te1:
|
|
filetypes = FileFullPaths.prep_filetypes(filetypes)
|
|
filename = filedialog.asksaveasfilename(filetypes=filetypes)
|
|
except TclError as te2:
|
|
filename = filedialog.asksaveasfilename()
|
|
if filename:
|
|
filepath.set(filename)
|
|
|
|
@staticmethod
|
|
def ask_nothing(filepath, filetypes=None):
|
|
""" Method that does nothing, used for disabling open/save pop up """
|
|
return
|
|
|
|
def ask_combo(self, filepath, filetypes):
|
|
actions_open_type = self.option['actions_open_type']
|
|
task_name = actions_open_type['task_name']
|
|
chosen_action = self.utils.get_chosen_action(task_name)
|
|
action = getattr(self, "ask_" + actions_open_type[chosen_action])
|
|
filetypes = filetypes[chosen_action]
|
|
action(filepath, filetypes)
|
|
|
|
|
|
class ActionFrame(object):
|
|
"""Action Frame - Displays information and action controls """
|
|
|
|
def __init__(self, utils, page, command):
|
|
self.utils = utils
|
|
self.page = page
|
|
self.command = command
|
|
self.title = command.title()
|
|
|
|
def build_frame(self):
|
|
""" Add help display and Action buttons to the left frame of each
|
|
page """
|
|
frame = ttk.Frame(self.page)
|
|
frame.pack(fill=tk.BOTH, padx=(10, 5), side=tk.BOTTOM, anchor=tk.N)
|
|
|
|
self.add_action_button(frame)
|
|
self.add_util_buttons(frame)
|
|
|
|
def add_action_button(self, frame):
|
|
""" Add the action buttons for page """
|
|
actframe = ttk.Frame(frame)
|
|
actframe.pack(fill=tk.X, side=tk.LEFT, padx=5, pady=5)
|
|
|
|
btnact = ttk.Button(actframe,
|
|
text=self.title,
|
|
width=12,
|
|
command=lambda: self.utils.action_command(
|
|
self.command))
|
|
btnact.pack(side=tk.TOP)
|
|
Tooltip(btnact, text='Run the {} script'.format(self.title), wraplength=200)
|
|
self.utils.actionbtns[self.command] = btnact
|
|
|
|
def add_util_buttons(self, frame):
|
|
""" Add the section utility buttons """
|
|
utlframe = ttk.Frame(frame)
|
|
utlframe.pack(side=tk.RIGHT, padx=(5, 10), pady=5)
|
|
|
|
for utl in ('load', 'save', 'clear', 'reset'):
|
|
img = self.utils.icons[utl]
|
|
action = getattr(self.utils, utl + '_config')
|
|
btnutl = ttk.Button(utlframe,
|
|
image=img,
|
|
command=lambda cmd=action: cmd(self.command))
|
|
btnutl.pack(padx=2, side=tk.LEFT)
|
|
Tooltip(btnutl, text=utl.capitalize() + ' ' + self.title + ' config', wraplength=200)
|
|
|
|
|
|
class DisplayTab(object):
|
|
""" The display tabs """
|
|
|
|
def __init__(self, utils, notebook, display):
|
|
self.utils = utils
|
|
self.notebook = notebook
|
|
self.page = ttk.Frame(self.notebook)
|
|
self.display = display
|
|
self.title = self.display.title()
|
|
|
|
def build_tab(self):
|
|
""" Build the tab """
|
|
frame = ttk.Frame(self.page)
|
|
frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
|
|
if self.display == 'graph':
|
|
graphframe = GraphDisplay(frame, self.utils)
|
|
graphframe.create_graphs()
|
|
elif self.display == 'preview':
|
|
preview = PreviewDisplay(frame, self.utils.previewloc)
|
|
preview.update_preview()
|
|
else: # Dummy in a placeholder
|
|
lbl = ttk.Label(frame, text=self.display, width=15, anchor=tk.NW)
|
|
lbl.pack(padx=5, pady=5, side=tk.LEFT, anchor=tk.N)
|
|
|
|
self.notebook.add(self.page, text=self.title)
|
|
|
|
|
|
class GraphDisplay(object):
|
|
""" The Graph Tab of the Display section """
|
|
|
|
def __init__(self, frame, utils):
|
|
self.frame = frame
|
|
self.utils = utils
|
|
self.losskeys = None
|
|
|
|
self.graphpane = tk.PanedWindow(self.frame, sashrelief=tk.RAISED, orient=tk.VERTICAL)
|
|
self.graphpane.pack(fill=tk.BOTH, expand=True)
|
|
|
|
self.graphs = list()
|
|
|
|
def create_graphs(self):
|
|
""" create the graph frames when there are loss values to graph """
|
|
if not self.utils.lossdict:
|
|
self.frame.after(1000, self.create_graphs)
|
|
return
|
|
|
|
self.losskeys = sorted([key for key in self.utils.lossdict.keys()])
|
|
|
|
framecount = int(len(self.utils.lossdict) / 2)
|
|
for i in range(framecount):
|
|
self.add_graph(i)
|
|
|
|
self.monitor_state()
|
|
|
|
def add_graph(self, index):
|
|
""" Add a single graph to the graph window """
|
|
graphframe = ttk.Frame(self.graphpane)
|
|
self.graphpane.add(graphframe)
|
|
|
|
selectedkeys = self.losskeys[index * 2:(index + 1) * 2]
|
|
selectedloss = {key: self.utils.lossdict[key] for key in selectedkeys}
|
|
|
|
graph = Graph(graphframe, selectedloss, selectedkeys)
|
|
self.graphs.append(graph)
|
|
graph.build_graph()
|
|
|
|
def monitor_state(self):
|
|
""" Check there is a task still running. If not, destroy graphs
|
|
and reset graph display to waiting state """
|
|
if self.utils.lossdict:
|
|
self.frame.after(5000, self.monitor_state)
|
|
return
|
|
self.destroy_graphs()
|
|
self.create_graphs()
|
|
|
|
def destroy_graphs(self):
|
|
""" Destroy graphs when the process has stopped """
|
|
for graph in self.graphs:
|
|
del graph
|
|
self.graphs = list()
|
|
for child in self.graphpane.panes():
|
|
self.graphpane.remove(child)
|
|
|
|
|
|
class Graph(object):
|
|
""" Each graph to be displayed. Until training is run it is not known
|
|
how many graphs will be required, so they sit in their own class
|
|
ready to be created when requested """
|
|
|
|
def __init__(self, frame, loss, losskeys):
|
|
self.frame = frame
|
|
self.loss = loss
|
|
self.losskeys = losskeys
|
|
|
|
self.ylim = (100, 0)
|
|
|
|
style.use('ggplot')
|
|
|
|
self.fig = plt.figure(figsize=(4, 4), dpi=75)
|
|
self.ax1 = self.fig.add_subplot(1, 1, 1)
|
|
self.losslines = list()
|
|
self.trndlines = list()
|
|
|
|
def build_graph(self):
|
|
""" Update the plot area with loss values and cycle through to
|
|
animate """
|
|
self.ax1.set_xlabel('Iterations')
|
|
self.ax1.set_ylabel('Loss')
|
|
self.ax1.set_ylim(0.00, 0.01)
|
|
self.ax1.set_xlim(0, 1)
|
|
|
|
losslbls = [lbl.replace('_', ' ').title() for lbl in self.losskeys]
|
|
for idx, linecol in enumerate(['blue', 'red']):
|
|
self.losslines.extend(self.ax1.plot(0, 0,
|
|
color=linecol,
|
|
linewidth=1,
|
|
label=losslbls[idx]))
|
|
for idx, linecol in enumerate(['navy', 'firebrick']):
|
|
lbl = losslbls[idx]
|
|
lbl = 'Trend{}'.format(lbl[lbl.rfind(' '):])
|
|
self.trndlines.extend(self.ax1.plot(0, 0,
|
|
color=linecol,
|
|
linewidth=2,
|
|
label=lbl))
|
|
|
|
self.ax1.legend(loc='upper right')
|
|
|
|
plt.subplots_adjust(left=0.075, bottom=0.075, right=0.95, top=0.95,
|
|
wspace=0.2, hspace=0.2)
|
|
|
|
plotcanvas = FigureCanvasTkAgg(self.fig, self.frame)
|
|
plotcanvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True)
|
|
ani = animation.FuncAnimation(self.fig, self.animate, interval=2000, blit=False)
|
|
plotcanvas.draw()
|
|
|
|
def animate(self, i):
|
|
""" Read loss data and apply to graph """
|
|
loss = [self.loss[key][:] for key in self.losskeys]
|
|
|
|
xlim = self.recalculate_axes(loss)
|
|
|
|
xrng = [x for x in range(xlim)]
|
|
|
|
self.raw_plot(xrng, loss)
|
|
|
|
if xlim > 10:
|
|
self.trend_plot(xrng, loss)
|
|
|
|
def recalculate_axes(self, loss):
|
|
""" Recalculate the latest x and y axes limits from latest data """
|
|
ymin = floor(min([min(lossvals) for lossvals in loss]) * 100) / 100
|
|
ymax = ceil(max([max(lossvals) for lossvals in loss]) * 100) / 100
|
|
|
|
if ymin < self.ylim[0] or ymax > self.ylim[1]:
|
|
self.ylim = (ymin, ymax)
|
|
self.ax1.set_ylim(self.ylim[0], self.ylim[1])
|
|
|
|
xlim = len(loss[0])
|
|
xlim = 2 if xlim == 1 else xlim
|
|
self.ax1.set_xlim(0, xlim - 1)
|
|
|
|
return xlim
|
|
|
|
def raw_plot(self, x_range, loss):
|
|
""" Raw value plotting """
|
|
for idx, lossvals in enumerate(loss):
|
|
self.losslines[idx].set_data(x_range, lossvals)
|
|
|
|
def trend_plot(self, x_range, loss):
|
|
""" Trend value plotting """
|
|
for idx, lossvals in enumerate(loss):
|
|
fit = numpy.polyfit(x_range, lossvals, 3)
|
|
poly = numpy.poly1d(fit)
|
|
self.trndlines[idx].set_data(x_range, poly(x_range))
|
|
|
|
|
|
class PreviewDisplay(object):
|
|
""" The Preview tab of the Display section """
|
|
|
|
def __init__(self, frame, previewloc):
|
|
self.frame = frame
|
|
self.previewimg = None
|
|
self.errcount = 0
|
|
self.previewloc = previewloc
|
|
|
|
self.previewlbl = ttk.Label(self.frame, image=None, anchor=tk.NW)
|
|
self.previewlbl.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
|
|
|
|
def update_preview(self):
|
|
""" Display the image if it exists or a place holder if it doesn't """
|
|
self.load_preview()
|
|
if self.previewimg is None:
|
|
self.previewlbl.config(image=None)
|
|
else:
|
|
self.previewlbl.config(image=self.previewimg)
|
|
self.previewlbl.after(1000, self.update_preview)
|
|
|
|
def load_preview(self):
|
|
""" Load the preview image into tk PhotoImage """
|
|
if os.path.exists(self.previewloc):
|
|
try:
|
|
self.previewimg = tk.PhotoImage(file=self.previewloc)
|
|
self.errcount = 0
|
|
except TclError:
|
|
# 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
|
|
if self.errcount < 10:
|
|
self.errcount += 1
|
|
self.previewimg = None
|
|
else:
|
|
print('Error reading the preview file')
|
|
else:
|
|
self.previewimg = None
|
|
|
|
|
|
class FaceswapControl(object):
|
|
""" Control the underlying Faceswap tasks """
|
|
__group_processes = ["effmpeg"]
|
|
|
|
def __init__(self, utils, calling_file="faceswap.py"):
|
|
self.pathexecscript = os.path.join(PATHSCRIPT, calling_file)
|
|
self.utils = utils
|
|
|
|
self.command = None
|
|
self.args = None
|
|
self.process = None
|
|
self.lenloss = 0
|
|
|
|
def prepare(self, options, command):
|
|
""" Prepare for running the subprocess """
|
|
self.command = command
|
|
self.utils.runningtask = True
|
|
self.utils.change_action_button()
|
|
self.utils.guitext['status'].set('Executing - ' + self.command + '.py')
|
|
print('Loading...')
|
|
self.args = ['python', '-u', self.pathexecscript, self.command]
|
|
self.build_args(options)
|
|
|
|
def build_args(self, options):
|
|
""" Build the faceswap command and arguments list """
|
|
for item in options[self.command]:
|
|
optval = str(item.get('value', '').get())
|
|
opt = item['opts'][0]
|
|
if optval == 'False' or optval == '':
|
|
continue
|
|
elif optval == 'True':
|
|
if self.command == 'train' and opt == '-p': # Embed the preview pane
|
|
self.args.append('-gui')
|
|
else:
|
|
self.args.append(opt)
|
|
else:
|
|
self.args.extend((opt, optval))
|
|
|
|
def execute_script(self):
|
|
""" Execute the requested Faceswap Script """
|
|
kwargs = {'stdout': PIPE,
|
|
'stderr': PIPE,
|
|
'bufsize': 1,
|
|
'universal_newlines': True}
|
|
if self.command in self.__group_processes:
|
|
kwargs['preexec_fn'] = os.setsid
|
|
if os.name == 'nt':
|
|
kwargs['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP
|
|
self.process = Popen(self.args, **kwargs)
|
|
self.thread_stdout()
|
|
self.thread_stderr()
|
|
|
|
def read_stdout(self):
|
|
""" Read stdout from the subprocess. If training, pass the loss
|
|
values to Queue """
|
|
while True:
|
|
output = self.process.stdout.readline()
|
|
if output == '' and self.process.poll() is not None:
|
|
break
|
|
if output:
|
|
if self.command == 'train' and str.startswith(output, '['):
|
|
self.capture_loss(output)
|
|
print(output.strip())
|
|
returncode = self.process.poll()
|
|
self.utils.runningtask = False
|
|
self.utils.change_action_button()
|
|
self.set_final_status(returncode)
|
|
print('Process exited.')
|
|
|
|
def read_stderr(self):
|
|
""" Read stdout from the subprocess. If training, pass the loss
|
|
values to Queue """
|
|
while True:
|
|
output = self.process.stderr.readline()
|
|
if output == '' and self.process.poll() is not None:
|
|
break
|
|
print(output.strip(), file=sys.stderr)
|
|
|
|
def thread_stdout(self):
|
|
""" Put the subprocess stdout so that it can be read without
|
|
blocking """
|
|
thread = Thread(target=self.read_stdout)
|
|
thread.daemon = True
|
|
thread.start()
|
|
|
|
def thread_stderr(self):
|
|
""" Put the subprocess stderr so that it can be read without
|
|
blocking """
|
|
thread = Thread(target=self.read_stderr)
|
|
thread.daemon = True
|
|
thread.start()
|
|
|
|
def capture_loss(self, string):
|
|
""" Capture loss values from stdout """
|
|
# TODO: Remove this hideous hacky fix. When the subprocess is terminated and
|
|
# the loss dictionary is reset, 1 set of loss values ALWAYS slips through
|
|
# and appends to the lossdict AFTER the subprocess has closed meaning that
|
|
# checks on whether the dictionary is empty fail.
|
|
# Therefore if the size of current loss dictionary is smaller than the
|
|
# previous loss dictionary, assume that the process has been terminated
|
|
# and reset it.
|
|
# I have tried and failed to empty the subprocess stdout with:
|
|
# sys.exit() on the stdout/err threads (no effect)
|
|
# sys.stdout/stderr.flush (no effect)
|
|
# thread.join (locks the whole process up, because the stdout thread
|
|
# stubbornly refuses to release its last line)
|
|
|
|
currentlenloss = len(self.utils.lossdict)
|
|
if self.lenloss > currentlenloss:
|
|
self.utils.lossdict = dict()
|
|
self.lenloss = 0
|
|
return
|
|
self.lenloss = currentlenloss
|
|
|
|
loss = re.findall(r'([a-zA-Z_]+):.*?(\d+\.\d+)', string)
|
|
|
|
if len(loss) < 2:
|
|
return
|
|
|
|
if not self.utils.lossdict:
|
|
self.utils.lossdict.update((item[0], []) for item in loss)
|
|
|
|
for item in loss:
|
|
self.utils.lossdict[item[0]].append(float(item[1]))
|
|
|
|
def terminate(self):
|
|
""" Terminate the subprocess """
|
|
if self.command == 'train':
|
|
print('Sending Exit Signal', flush=True)
|
|
try:
|
|
now = time()
|
|
if os.name == 'nt':
|
|
os.kill(self.process.pid, signal.CTRL_BREAK_EVENT)
|
|
else:
|
|
self.process.send_signal(signal.SIGINT)
|
|
while True:
|
|
timeelapsed = time() - now
|
|
if self.process.poll() is not None:
|
|
break
|
|
if timeelapsed > 30:
|
|
raise ValueError('Timeout reached sending Exit Signal')
|
|
return
|
|
except ValueError as err:
|
|
print(err)
|
|
elif self.command in self.__group_processes:
|
|
print('Terminating Process Group...')
|
|
pgid = os.getpgid(self.process.pid)
|
|
try:
|
|
os.killpg(pgid, signal.SIGINT)
|
|
self.process.wait(timeout=10)
|
|
print('Terminated')
|
|
except TimeoutExpired:
|
|
print('Termination timed out. Killing Process Group...')
|
|
os.killpg(pgid, signal.SIGKILL)
|
|
print('Killed')
|
|
else:
|
|
print('Terminating Process...')
|
|
try:
|
|
self.process.terminate()
|
|
self.process.wait(timeout=10)
|
|
print('Terminated')
|
|
except TimeoutExpired:
|
|
print('Termination timed out. Killing Process...')
|
|
self.process.kill()
|
|
print('Killed')
|
|
|
|
def set_final_status(self, returncode):
|
|
""" Set the status bar output based on subprocess return code """
|
|
if returncode == 0 or returncode == 3221225786:
|
|
status = 'Ready'
|
|
elif returncode == -15:
|
|
status = 'Terminated - {}.py'.format(self.command)
|
|
elif returncode == -9:
|
|
status = 'Killed - {}.py'.format(self.command)
|
|
elif returncode == -6:
|
|
status = 'Aborted - {}.py'.format(self.command)
|
|
else:
|
|
status = 'Failed - {}.py. Return Code: {}'.format(self.command, returncode)
|
|
self.utils.guitext['status'].set(status)
|
|
|
|
|
|
class Gui(object):
|
|
""" The GUI process. """
|
|
def __init__(self, arguments, subparsers):
|
|
# Don't try to load the GUI if there is no display or there are
|
|
# problems importing tkinter
|
|
if not self.check_display() or not self.check_tkinter_available():
|
|
return
|
|
|
|
cmd = sys.argv
|
|
|
|
# If not running in gui mode return before starting to create a window
|
|
if 'gui' not in cmd:
|
|
return
|
|
|
|
self.args = arguments
|
|
self.opts = self.extract_options(subparsers)
|
|
self.utils = Utils(self.opts, calling_file=cmd[0])
|
|
self.root = FaceswapGui(self.utils, calling_file=cmd[0])
|
|
|
|
@staticmethod
|
|
def check_display():
|
|
""" Check whether there is a display to output the GUI. If running on
|
|
Windows then assume not running in headless mode """
|
|
if not os.environ.get('DISPLAY', None) and os.name != 'nt':
|
|
if os.name == 'posix':
|
|
print('macOS users need to install XQuartz. '
|
|
'See https://support.apple.com/en-gb/HT201341')
|
|
return False
|
|
return True
|
|
|
|
@staticmethod
|
|
def check_tkinter_available():
|
|
""" Check whether TkInter is installed on user's machine """
|
|
tkinter_vars = [tk, ttk, filedialog, messagebox, TclError]
|
|
if any(var is None for var in tkinter_vars):
|
|
print(
|
|
"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.\n"
|
|
"You can try:\n"
|
|
" Windows/macOS: Install ActiveTcl Community "
|
|
"Edition from "
|
|
"www.activestate.com\n"
|
|
" Ubuntu/Mint/Debian: sudo apt install python3-tk\n"
|
|
" Arch: sudo pacman -S tk\n"
|
|
" CentOS/Redhat: sudo yum install tkinter\n"
|
|
" Fedora: sudo dnf install python3-tkinter\n",
|
|
file=sys.stderr)
|
|
return False
|
|
return True
|
|
|
|
def extract_options(self, subparsers):
|
|
""" Extract the existing ArgParse Options """
|
|
opts = {cmd: subparsers[cmd].argument_list + subparsers[cmd].optional_arguments
|
|
for cmd in subparsers.keys()}
|
|
for command in opts.values():
|
|
for opt in command:
|
|
if opt.get('help', '') == SUPPRESS:
|
|
command.remove(opt)
|
|
ctl, sysbrowser, filetypes, actions_open_types = self.set_control(opt)
|
|
opt['control_title'] = self.set_control_title(
|
|
opt.get('opts', ''))
|
|
opt['control'] = ctl
|
|
opt['filesystem_browser'] = sysbrowser
|
|
opt['filetypes'] = filetypes
|
|
opt['actions_open_types'] = actions_open_types
|
|
return opts
|
|
|
|
@staticmethod
|
|
def set_control_title(opts):
|
|
""" Take the option switch and format it nicely """
|
|
ctltitle = opts[1] if len(opts) == 2 else opts[0]
|
|
ctltitle = ctltitle.replace('-', ' ').replace('_', ' ').strip().title()
|
|
return ctltitle
|
|
|
|
@staticmethod
|
|
def set_control(option):
|
|
""" Set the control and filesystem browser to use for each option """
|
|
sysbrowser = None
|
|
filetypes = None
|
|
actions_open_type = None
|
|
ctl = ttk.Entry
|
|
if option.get('action', '') == FullPaths:
|
|
sysbrowser = 'folder'
|
|
elif option.get('action', '') == DirFullPaths:
|
|
sysbrowser = 'folder'
|
|
elif option.get('action', '') == FileFullPaths:
|
|
sysbrowser = 'load'
|
|
filetypes = option.get('filetypes', None)
|
|
elif option.get('action', '') == ComboFullPaths:
|
|
sysbrowser = 'combo'
|
|
actions_open_type = option['actions_open_type']
|
|
filetypes = option.get('filetypes', None)
|
|
elif option.get('choices', '') != '':
|
|
ctl = ttk.Combobox
|
|
elif option.get('action', '') == 'store_true':
|
|
ctl = ttk.Checkbutton
|
|
return ctl, sysbrowser, filetypes, actions_open_type
|
|
|
|
def process(self):
|
|
""" Builds the GUI """
|
|
self.utils.debugconsole = self.args.debug
|
|
self.root.build_gui()
|
|
self.root.gui.mainloop()
|