1
0
Fork 0
mirror of https://github.com/deepfakes/faceswap synced 2025-06-07 10:43:27 -04:00
faceswap/lib/gui/popup_session.py
torzdf 6a3b674bef
Rebase code (#1326)
* Remove tensorflow_probability requirement

* setup.py - fix progress bars

* requirements.txt: Remove pre python 3.9 packages

* update apple requirements.txt

* update INSTALL.md

* Remove python<3.9 code

* setup.py - fix Windows Installer

* typing: python3.9 compliant

* Update pytest and readthedocs python versions

* typing fixes

* Python Version updates
  - Reduce max version to 3.10
  - Default to 3.10 in installers
  - Remove incompatible 3.11 tests

* Update dependencies

* Downgrade imageio dep for Windows

* typing: merge optional unions and fixes

* Updates
  - min python version 3.10
  - typing to python 3.10 spec
  - remove pre-tf2.10 code
  - Add conda tests

* train: re-enable optimizer saving

* Update dockerfiles

* Update setup.py
  - Apple Conda deps to setup.py
  - Better Cuda + dependency handling

* bugfix: Patch logging to prevent Autograph errors

* Update dockerfiles

* Setup.py - Setup.py - stdout to utf-8

* Add more OSes to github Actions

* suppress mac-os end to end test
2023-06-27 11:27:47 +01:00

579 lines
21 KiB
Python

#!/usr/bin python3
""" Pop-up Graph launched from the Analysis tab of the Faceswap GUI """
import csv
import gettext
import logging
import tkinter as tk
from dataclasses import dataclass, field
from tkinter import ttk
from .control_helper import ControlBuilder, ControlPanelOption
from .custom_widgets import Tooltip
from .display_graph import SessionGraph
from .analysis import Calculations, Session
from .utils import FileHandler, get_images, LongRunningTask
logger = logging.getLogger(__name__) # pylint: disable=invalid-name
# LOCALES
_LANG = gettext.translation("gui.tooltips", localedir="locales", fallback=True)
_ = _LANG.gettext
@dataclass
class SessionTKVars:
""" Dataclass for holding the tk variables required for the session popup
Parameters
----------
buildgraph: :class:`tkinter.BooleanVar`
Trigger variable to indicate the graph should be rebuilt
status: :class:`tkinter.StringVar`
The variable holding the current status of the popup window
display: :class:`tkinter.StringVar`
Variable indicating the type of information to be displayed
scale: :class:`tkinter.StringVar`
Variable indicating whether to display as log or linear data
raw: :class:`tkinter.BooleanVar`
Variable to indicate raw data should be displayed
trend: :class:`tkinter.BooleanVar`
Variable to indicate that trend data should be displayed
avg: :class:`tkinter.BooleanVar`
Variable to indicate that rolling average data should be displayed
smoothed: :class:`tkinter.BooleanVar`
Variable to indicate that smoothed data should be displayed
outliers: :class:`tkinter.BooleanVar`
Variable to indicate that outliers should be displayed
loss_keys: dict
Dictionary of names to :class:`tkinter.BooleanVar` indicating whether specific loss items
should be displayed
avgiterations: :class:`tkinter.IntVar`
The number of iterations to use for rolling average
smoothamount: :class:`tkinter.DoubleVar`
The amount of smoothing to apply for smoothed data
"""
buildgraph: tk.BooleanVar
status: tk.StringVar
display: tk.StringVar
scale: tk.StringVar
raw: tk.BooleanVar
trend: tk.BooleanVar
avg: tk.BooleanVar
smoothed: tk.BooleanVar
outliers: tk.BooleanVar
avgiterations: tk.IntVar
smoothamount: tk.DoubleVar
loss_keys: dict[str, tk.BooleanVar] = field(default_factory=dict)
class SessionPopUp(tk.Toplevel):
""" Pop up for detailed graph/stats for selected session.
session_id: int or `"Total"`
The session id number for the selected session from the Analysis tab. Should be the string
`"Total"` if all sessions are being graphed
data_points: int
The number of iterations in the selected session
"""
def __init__(self, session_id: int, data_points: int) -> None:
logger.debug("Initializing: %s: (session_id: %s, data_points: %s)",
self.__class__.__name__, session_id, data_points)
super().__init__()
self._thread: LongRunningTask | None = None # Thread for loading data in background
self._default_view = "avg" if data_points > 1000 else "smoothed"
self._session_id = None if session_id == "Total" else int(session_id)
self._graph_frame = ttk.Frame(self)
self._graph: SessionGraph | None = None
self._display_data: Calculations | None = None
self._vars = self._set_vars()
self._graph_initialised = False
optsframe = self._layout_frames()
self._build_options(optsframe)
self._lbl_loading = ttk.Label(self._graph_frame, text="Loading Data...", anchor=tk.CENTER)
self._lbl_loading.pack(fill=tk.BOTH, expand=True)
self.update_idletasks()
self._compile_display_data()
logger.debug("Initialized: %s", self.__class__.__name__)
def _set_vars(self) -> SessionTKVars:
""" Set status tkinter String variable and tkinter Boolean variable to callback when the
graph is ready to build.
Returns
-------
:class:`SessionTKVars`
The tkinter Variables for the pop up graph
"""
logger.debug("Setting tk graph build variable and internal variables")
retval = SessionTKVars(buildgraph=tk.BooleanVar(),
status=tk.StringVar(),
display=tk.StringVar(),
scale=tk.StringVar(),
raw=tk.BooleanVar(),
trend=tk.BooleanVar(),
avg=tk.BooleanVar(),
smoothed=tk.BooleanVar(),
outliers=tk.BooleanVar(),
avgiterations=tk.IntVar(),
smoothamount=tk.DoubleVar())
retval.buildgraph.set(False)
retval.buildgraph.trace("w", self._graph_build)
return retval
def _layout_frames(self) -> ttk.Frame:
""" Top level container frames """
logger.debug("Layout frames")
leftframe = ttk.Frame(self)
sep = ttk.Frame(self, width=2, relief=tk.RIDGE)
self._graph_frame.pack(side=tk.RIGHT, fill=tk.BOTH, pady=5, expand=True)
sep.pack(fill=tk.Y, side=tk.LEFT)
leftframe.pack(side=tk.LEFT, expand=False, fill=tk.BOTH, pady=5)
logger.debug("Laid out frames")
return leftframe
def _build_options(self, frame: ttk.Frame) -> None:
""" Build Options into the options frame.
Parameters
----------
frame: :class:`tkinter.ttk.Frame`
The frame that the options reside in
"""
logger.debug("Building Options")
self._opts_combobox(frame)
self._opts_checkbuttons(frame)
self._opts_loss_keys(frame)
self._opts_slider(frame)
self._opts_buttons(frame)
sep = ttk.Frame(frame, height=2, relief=tk.RIDGE)
sep.pack(fill=tk.X, pady=(5, 0), side=tk.BOTTOM)
logger.debug("Built Options")
def _opts_combobox(self, frame: ttk.Frame) -> None:
""" Add the options combo boxes.
Parameters
----------
frame: :class:`tkinter.ttk.Frame`
The frame that the options reside in
"""
logger.debug("Building Combo boxes")
choices = {"Display": ("Loss", "Rate"), "Scale": ("Linear", "Log")}
for item in ["Display", "Scale"]:
var: tk.StringVar = getattr(self._vars, item.lower())
cmbframe = ttk.Frame(frame)
lblcmb = ttk.Label(cmbframe, text=f"{item}:", width=7, anchor=tk.W)
cmb = ttk.Combobox(cmbframe, textvariable=var, width=10)
cmb["values"] = choices[item]
cmb.current(0)
cmd = self._option_button_reload if item == "Display" else self._graph_scale
var.trace("w", cmd)
hlp = self._set_help(item)
Tooltip(cmbframe, text=hlp, wrap_length=200)
cmb.pack(fill=tk.X, side=tk.RIGHT)
lblcmb.pack(padx=(0, 2), side=tk.LEFT)
cmbframe.pack(fill=tk.X, pady=5, padx=5, side=tk.TOP)
logger.debug("Built Combo boxes")
def _opts_checkbuttons(self, frame: ttk.Frame) -> None:
""" Add the options check buttons.
Parameters
----------
frame: :class:`tkinter.ttk.Frame`
The frame that the options reside in
"""
logger.debug("Building Check Buttons")
self._add_section(frame, "Display")
for item in ("raw", "trend", "avg", "smoothed", "outliers"):
if item == "avg":
text = "Show Rolling Average"
elif item == "outliers":
text = "Flatten Outliers"
else:
text = f"Show {item.title()}"
var: tk.BooleanVar = getattr(self._vars, item)
if item == self._default_view:
var.set(True)
ctl = ttk.Checkbutton(frame, variable=var, text=text)
hlp = self._set_help(item)
Tooltip(ctl, text=hlp, wrap_length=200)
ctl.pack(side=tk.TOP, padx=5, pady=5, anchor=tk.W)
logger.debug("Built Check Buttons")
def _opts_loss_keys(self, frame: ttk.Frame) -> None:
""" Add loss key selections.
Parameters
----------
frame: :class:`tkinter.ttk.Frame`
The frame that the options reside in
"""
logger.debug("Building Loss Key Check Buttons")
loss_keys = Session.get_loss_keys(self._session_id)
lk_vars = {}
section_added = False
for loss_key in sorted(loss_keys):
if loss_key.startswith("total"):
continue
text = loss_key.replace("_", " ").title()
helptext = _("Display {}").format(text)
var = tk.BooleanVar()
var.set(True)
lk_vars[loss_key] = var
if len(loss_keys) == 1:
# Don't display if there's only one item
break
if not section_added:
self._add_section(frame, "Keys")
section_added = True
ctl = ttk.Checkbutton(frame, variable=var, text=text)
Tooltip(ctl, text=helptext, wrap_length=200)
ctl.pack(side=tk.TOP, padx=5, pady=5, anchor=tk.W)
self._vars.loss_keys = lk_vars
logger.debug("Built Loss Key Check Buttons")
def _opts_slider(self, frame: ttk.Frame) -> None:
""" Add the options entry boxes.
Parameters
----------
frame: :class:`tkinter.ttk.Frame`
The frame that the options reside in
"""
self._add_section(frame, "Parameters")
logger.debug("Building Slider Controls")
for item in ("avgiterations", "smoothamount"):
if item == "avgiterations":
dtype: type[int] | type[float] = int
text = "Iterations to Average:"
default: int | float = 500
rounding = 25
min_max: tuple[int, int | float] = (25, 2500)
elif item == "smoothamount":
dtype = float
text = "Smoothing Amount:"
default = 0.90
rounding = 2
min_max = (0, 0.99)
slider = ControlPanelOption(text,
dtype,
default=default,
rounding=rounding,
min_max=min_max,
helptext=self._set_help(item))
setattr(self._vars, item, slider.tk_var)
ControlBuilder(frame, slider, 1, 19, None, "Analysis.", True)
logger.debug("Built Sliders")
def _opts_buttons(self, frame: ttk.Frame) -> None:
""" Add the option buttons.
Parameters
----------
frame: :class:`tkinter.ttk.Frame`
The frame that the options reside in
"""
logger.debug("Building Buttons")
btnframe = ttk.Frame(frame)
lblstatus = ttk.Label(btnframe,
width=40,
textvariable=self._vars.status,
anchor=tk.W)
for btntype in ("reload", "save"):
cmd = getattr(self, f"_option_button_{btntype}")
btn = ttk.Button(btnframe,
image=get_images().icons[btntype],
command=cmd)
hlp = self._set_help(btntype)
Tooltip(btn, text=hlp, wrap_length=200)
btn.pack(padx=2, side=tk.RIGHT)
lblstatus.pack(side=tk.LEFT, anchor=tk.W, fill=tk.X, expand=True)
btnframe.pack(fill=tk.X, pady=5, padx=5, side=tk.BOTTOM)
logger.debug("Built Buttons")
@classmethod
def _add_section(cls, frame: ttk.Frame, title: str) -> None:
""" Add a separator and section title between options
Parameters
----------
frame: :class:`tkinter.ttk.Frame`
The frame that the options reside in
title: str
The section title to display
"""
sep = ttk.Frame(frame, height=2, relief=tk.SOLID)
lbl = ttk.Label(frame, text=title)
lbl.pack(side=tk.TOP, padx=5, pady=0, anchor=tk.CENTER)
sep.pack(fill=tk.X, pady=(5, 0), side=tk.TOP)
def _option_button_save(self) -> None:
""" Action for save button press. """
logger.debug("Saving File")
savefile = FileHandler("save", "csv").return_file
if not savefile:
logger.debug("Save Cancelled")
return
logger.debug("Saving to: %s", savefile)
assert self._display_data is not None
save_data = self._display_data.stats
fieldnames = sorted(key for key in save_data.keys())
with savefile as outfile:
csvout = csv.writer(outfile, delimiter=",")
csvout.writerow(fieldnames)
csvout.writerows(zip(*[save_data[key] for key in fieldnames]))
def _option_button_reload(self, *args) -> None: # pylint: disable=unused-argument
""" Action for reset button press and checkbox changes.
Parameters
----------
args: tuple
Required for TK Callback but unused
"""
logger.debug("Refreshing Graph")
if not self._graph_initialised:
return
valid = self._compile_display_data()
if not valid:
logger.debug("Invalid data")
return
assert self._graph is not None
self._graph.refresh(self._display_data,
self._vars.display.get(),
self._vars.scale.get())
logger.debug("Refreshed Graph")
def _graph_scale(self, *args) -> None: # pylint: disable=unused-argument
""" Action for changing graph scale.
Parameters
----------
args: tuple
Required for TK Callback but unused
"""
assert self._graph is not None
if not self._graph_initialised:
return
self._graph.set_yscale_type(self._vars.scale.get())
@classmethod
def _set_help(cls, action: str) -> str:
""" Set the help text for option buttons.
Parameters
----------
action: str
The action to get the help text for
Returns
-------
str
The help text for the given action
"""
lookup = {
"reload": _("Refresh graph"),
"save": _("Save display data to csv"),
"avgiterations": _("Number of data points to sample for rolling average"),
"smoothamount": _("Set the smoothing amount. 0 is no smoothing, 0.99 is maximum "
"smoothing"),
"outliers": _("Flatten data points that fall more than 1 standard deviation from the "
"mean to the mean value."),
"avg": _("Display rolling average of the data"),
"smoothed": _("Smooth the data"),
"raw": _("Display raw data"),
"trend": _("Display polynormal data trend"),
"display": _("Set the data to display"),
"scale": _("Change y-axis scale")}
return lookup.get(action.lower(), "")
def _compile_display_data(self) -> bool:
""" Compile the data to be displayed.
Returns
-------
bool
``True`` if there is valid data to display, ``False`` if not
"""
if self._thread is None:
logger.debug("Compiling Display Data in background thread")
loss_keys = [key for key, val in self._vars.loss_keys.items()
if val.get()]
logger.debug("Selected loss_keys: %s", loss_keys)
selections = self._selections_to_list()
if not self._check_valid_selection(loss_keys, selections):
logger.warning("No data to display. Not refreshing")
return False
self._vars.status.set("Loading Data...")
if self._graph is not None:
self._graph.pack_forget()
self._lbl_loading.pack(fill=tk.BOTH, expand=True)
self.update_idletasks()
kwargs = {"session_id": self._session_id,
"display": self._vars.display.get(),
"loss_keys": loss_keys,
"selections": selections,
"avg_samples": self._vars.avgiterations.get(),
"smooth_amount": self._vars.smoothamount.get(),
"flatten_outliers": self._vars.outliers.get()}
self._thread = LongRunningTask(target=self._get_display_data,
kwargs=kwargs,
widget=self)
self._thread.start()
self.after(1000, self._compile_display_data)
return True
if not self._thread.complete.is_set():
logger.debug("Popup Data not yet available")
self.after(1000, self._compile_display_data)
return True
logger.debug("Getting Popup from background Thread")
self._display_data = self._thread.get_result()
self._thread = None
if not self._check_valid_data():
logger.warning("No valid data to display. Not refreshing")
self._vars.status.set("")
return False
logger.debug("Compiled Display Data")
self._vars.buildgraph.set(True)
return True
@classmethod
def _get_display_data(cls, **kwargs) -> Calculations:
""" Get the display data in a LongRunningTask.
Parameters
----------
kwargs: dict
The keyword arguments to pass to `lib.gui.analysis.Calculations`
Returns
-------
:class:`lib.gui.analysis.Calculations`
The summarized results for the given session
"""
return Calculations(**kwargs)
def _check_valid_selection(self, loss_keys: list[str], selections: list[str]) -> bool:
""" Check that there will be data to display.
Parameters
----------
loss_keys: list
The selected loss to display
selections: list
The selected checkbox options
Returns
-------
bool
``True` if there is data to be displayed, otherwise ``False``
"""
display = self._vars.display.get().lower()
logger.debug("Validating selection. (loss_keys: %s, selections: %s, display: %s)",
loss_keys, selections, display)
if not selections or (display == "loss" and not loss_keys):
return False
return True
def _check_valid_data(self) -> bool:
""" Check that the selections holds valid data to display
NB: len-as-condition is used as data could be a list or a numpy array
Returns
-------
bool
``True` if there is data to be displayed, otherwise ``False``
"""
assert self._display_data is not None
logger.debug("Validating data. %s",
{key: len(val) for key, val in self._display_data.stats.items()})
if any(len(val) == 0 # pylint:disable=len-as-condition
for val in self._display_data.stats.values()):
return False
return True
def _selections_to_list(self) -> list[str]:
""" Compile checkbox selections to a list.
Returns
-------
list
The selected options from the check-boxes
"""
logger.debug("Compiling selections to list")
selections = []
for item in ("raw", "trend", "avg", "smoothed"):
var: tk.BooleanVar = getattr(self._vars, item)
if var.get():
selections.append(item)
logger.debug("Compiling selections to list: %s", selections)
return selections
def _graph_build(self, *args) -> None: # pylint:disable=unused-argument
""" Build the graph in the top right paned window
Parameters
----------
args: tuple
Required for TK Callback but unused
"""
if not self._vars.buildgraph.get():
return
self._vars.status.set("Loading Data...")
logger.debug("Building Graph")
self._lbl_loading.pack_forget()
self.update_idletasks()
if self._graph is None:
graph = SessionGraph(self._graph_frame,
self._display_data,
self._vars.display.get(),
self._vars.scale.get())
graph.pack(expand=True, fill=tk.BOTH)
graph.build()
self._graph = graph
self._graph_initialised = True
else:
self._graph.refresh(self._display_data,
self._vars.display.get(),
self._vars.scale.get())
self._graph.pack(fill=tk.BOTH, expand=True)
self._vars.status.set("")
self._vars.buildgraph.set(False)
logger.debug("Built Graph")