1
0
Fork 0
mirror of https://github.com/deepfakes/faceswap synced 2025-06-07 10:43:27 -04:00

bugfix: Manual tool. Allow working with image folders at EEN values > 1

This commit is contained in:
torzdf 2024-07-05 18:46:10 +01:00
parent ea63f1e64a
commit b6ac7b8039
14 changed files with 657 additions and 477 deletions

View file

@ -27,7 +27,6 @@ The Manual Module is the main entry point into the Manual Editor Tool.
~tools.manual.manual.Aligner
~tools.manual.manual.FrameLoader
~tools.manual.manual.Manual
~tools.manual.manual.TkGlobals
.. rubric:: Module
@ -55,6 +54,26 @@ detected_faces module
:undoc-members:
:show-inheritance:
globals module
==============
.. rubric:: Module Summary
.. autosummary::
:nosignatures:
~tools.manual.globals.CurrentFrame
~tools.manual.globals.TkGlobals
~tools.manual.globals.TKVars
.. rubric:: Module
.. automodule:: tools.manual.globals
:members:
:undoc-members:
:show-inheritance:
thumbnails module
==================

View file

@ -1466,7 +1466,10 @@ class SingleFrameLoader(ImagesLoader):
image = self._reader.get_data(index)[..., ::-1]
filename = self._dummy_video_framename(index)
else:
filename = self.file_list[index]
file_list = [f for idx, f in enumerate(self._file_list)
if idx not in self._skip_list] if self._skip_list else self._file_list
filename = file_list[index]
image = read_image(filename, raise_error=True)
filename = os.path.basename(filename)
logger.trace("index: %s, filename: %s image shape: %s", index, filename, image.shape)

View file

@ -69,7 +69,6 @@ class DetectedFaces():
logger.debug("Initialized %s", self.__class__.__name__)
# <<<< PUBLIC PROPERTIES >>>> #
# << SUBCLASSES >> #
@property
def extractor(self) -> manual.Aligner:
""" :class:`~tools.manual.manual.Aligner`: The pipeline for passing faces through the
@ -108,6 +107,11 @@ class DetectedFaces():
return self._tk_vars["face_count_changed"]
# << STATISTICS >> #
@property
def frame_list(self) -> list[str]:
""" list[str]: The list of all frame names that appear in the alignments file """
return list(self._alignments.data)
@property
def available_masks(self) -> dict[str, int]:
""" dict[str, int]: The mask type names stored in the alignments; type as key with the
@ -343,7 +347,7 @@ class _DiskIO():
self._tk_face_count_changed.set(True)
else:
self._tk_edited.set(True)
self._globals.tk_update.set(True)
self._globals.var_full_update.set(True)
@classmethod
def _add_remove_faces(cls,
@ -485,7 +489,7 @@ class Filter():
def frame_meets_criteria(self) -> bool:
""" bool: ``True`` if the current frame meets the selected filter criteria otherwise
``False`` """
filter_mode = self._globals.filter_mode
filter_mode = self._globals.var_filter_mode.get()
frame_faces = self._detected_faces.current_faces[self._globals.frame_index]
distance = self._filter_distance
@ -505,7 +509,7 @@ class Filter():
def _filter_distance(self) -> float:
""" float: The currently selected distance when Misaligned Faces filter is selected. """
try:
retval = self._globals.tk_filter_distance.get()
retval = self._globals.var_filter_distance.get()
except tk.TclError:
# Suppress error when distance box is empty
retval = 0
@ -514,22 +518,22 @@ class Filter():
@property
def count(self) -> int:
""" int: The number of frames that meet the filter criteria returned by
:attr:`~tools.manual.manual.TkGlobals.filter_mode`. """
:attr:`~tools.manual.manual.TkGlobals.var_filter_mode.get()`. """
face_count_per_index = self._detected_faces.face_count_per_index
if self._globals.filter_mode == "No Faces":
if self._globals.var_filter_mode.get() == "No Faces":
retval = sum(1 for fcount in face_count_per_index if fcount == 0)
elif self._globals.filter_mode == "Has Face(s)":
elif self._globals.var_filter_mode.get() == "Has Face(s)":
retval = sum(1 for fcount in face_count_per_index if fcount != 0)
elif self._globals.filter_mode == "Multiple Faces":
elif self._globals.var_filter_mode.get() == "Multiple Faces":
retval = sum(1 for fcount in face_count_per_index if fcount > 1)
elif self._globals.filter_mode == "Misaligned Faces":
elif self._globals.var_filter_mode.get() == "Misaligned Faces":
distance = self._filter_distance
retval = sum(1 for frame in self._detected_faces.current_faces
if any(face.aligned.average_distance > distance for face in frame))
else:
retval = len(face_count_per_index)
logger.trace("filter mode: %s, frame count: %s", # type:ignore[attr-defined]
self._globals.filter_mode, retval)
self._globals.var_filter_mode.get(), retval)
return retval
@property
@ -554,22 +558,22 @@ class Filter():
@property
def frames_list(self) -> list[int]:
""" list[int]: The list of frame indices that meet the filter criteria returned by
:attr:`~tools.manual.manual.TkGlobals.filter_mode`. """
:attr:`~tools.manual.manual.TkGlobals.var_filter_mode.get()`. """
face_count_per_index = self._detected_faces.face_count_per_index
if self._globals.filter_mode == "No Faces":
if self._globals.var_filter_mode.get() == "No Faces":
retval = [idx for idx, count in enumerate(face_count_per_index) if count == 0]
elif self._globals.filter_mode == "Multiple Faces":
elif self._globals.var_filter_mode.get() == "Multiple Faces":
retval = [idx for idx, count in enumerate(face_count_per_index) if count > 1]
elif self._globals.filter_mode == "Has Face(s)":
elif self._globals.var_filter_mode.get() == "Has Face(s)":
retval = [idx for idx, count in enumerate(face_count_per_index) if count != 0]
elif self._globals.filter_mode == "Misaligned Faces":
elif self._globals.var_filter_mode.get() == "Misaligned Faces":
distance = self._filter_distance
retval = [idx for idx, frame in enumerate(self._detected_faces.current_faces)
if any(face.aligned.average_distance > distance for face in frame)]
else:
retval = list(range(len(face_count_per_index)))
logger.trace("filter mode: %s, number_frames: %s", # type:ignore[attr-defined]
self._globals.filter_mode, len(retval))
self._globals.var_filter_mode.get(), len(retval))
return retval
@ -677,7 +681,7 @@ class FaceUpdate():
faces = self._faces_at_frame_index(frame_index)
del faces[face_index]
self._tk_face_count_changed.set(True)
self._globals.tk_update.set(True)
self._globals.var_full_update.set(True)
def bounding_box(self,
frame_index: int,
@ -717,7 +721,7 @@ class FaceUpdate():
face.top = pnt_y
face.height = height
face.add_landmarks_xy(self._extractor.get_landmarks(frame_index, face_index, aligner))
self._globals.tk_update.set(True)
self._globals.var_full_update.set(True)
def landmark(self,
frame_index: int, face_index: int,
@ -764,7 +768,7 @@ class FaceUpdate():
face.landmarks_xy[idx] = lmk
else:
face.landmarks_xy[landmark_index] += (shift_x, shift_y)
self._globals.tk_update.set(True)
self._globals.var_full_update.set(True)
def landmarks(self, frame_index: int, face_index: int, shift_x: int, shift_y: int) -> None:
""" Shift all of the landmarks and bounding box for the
@ -792,7 +796,7 @@ class FaceUpdate():
face.left += shift_x
face.top += shift_y
face.add_landmarks_xy(face.landmarks_xy + (shift_x, shift_y))
self._globals.tk_update.set(True)
self._globals.var_full_update.set(True)
def landmarks_rotate(self,
frame_index: int,
@ -818,7 +822,7 @@ class FaceUpdate():
rot_mat = cv2.getRotationMatrix2D(tuple(center.astype("float32")), angle, 1.)
face.add_landmarks_xy(cv2.transform(np.expand_dims(face.landmarks_xy, axis=0),
rot_mat).squeeze())
self._globals.tk_update.set(True)
self._globals.var_full_update.set(True)
def landmarks_scale(self,
frame_index: int,
@ -842,7 +846,7 @@ class FaceUpdate():
"""
face = self._faces_at_frame_index(frame_index)[face_index]
face.add_landmarks_xy(((face.landmarks_xy - center) * scale) + center)
self._globals.tk_update.set(True)
self._globals.var_full_update.set(True)
def mask(self, frame_index: int, face_index: int, mask: np.ndarray, mask_type: str) -> None:
""" Update the mask on an edit for the :class:`~lib.align.DetectedFace` object at
@ -862,7 +866,7 @@ class FaceUpdate():
face = self._faces_at_frame_index(frame_index)[face_index]
face.mask[mask_type].replace_mask(mask)
self._tk_edited.set(True)
self._globals.tk_update.set(True)
self._globals.var_full_update.set(True)
def copy(self, frame_index: int, direction: T.Literal["prev", "next"]) -> None:
""" Copy the alignments from the previous or next frame that has alignments
@ -903,7 +907,7 @@ class FaceUpdate():
faces.extend(copied)
self._tk_face_count_changed.set(True)
self._globals.tk_update.set(True)
self._globals.var_full_update.set(True)
def post_edit_trigger(self, frame_index: int, face_index: int) -> None:
""" Update the jpg thumbnail, the viewport thumbnail, the landmark masks and the aligned
@ -922,11 +926,11 @@ class FaceUpdate():
face.clear_all_identities()
aligned = AlignedFace(face.landmarks_xy,
image=self._globals.current_frame["image"],
image=self._globals.current_frame.image,
centering="head",
size=96)
assert aligned.face is not None
face.thumbnail = generate_thumbnail(aligned.face, size=96)
if self._globals.filter_mode == "Misaligned Faces":
if self._globals.var_filter_mode.get() == "Misaligned Faces":
self._detected_faces.tk_face_count_changed.set(True)
self._tk_edited.set(True)

View file

@ -38,7 +38,7 @@ class FacesFrame(ttk.Frame): # pylint:disable=too-many-ancestors
Parameters
----------
parent: :class:`ttk.PanedWindow`
parent: :class:`ttk.Frame`
The paned window that the faces frame resides in
tk_globals: :class:`~tools.manual.manual.TkGlobals`
The tkinter variables that apply to the whole of the GUI
@ -48,7 +48,7 @@ class FacesFrame(ttk.Frame): # pylint:disable=too-many-ancestors
The section of the Manual Tool that holds the frames viewer
"""
def __init__(self,
parent: ttk.PanedWindow,
parent: ttk.Frame,
tk_globals: TkGlobals,
detected_faces: DetectedFaces,
display_frame: DisplayFrame) -> None:
@ -282,7 +282,7 @@ class FacesViewer(tk.Canvas): # pylint:disable=too-many-ancestors
def face_size(self) -> int:
""" int: The currently selected thumbnail size in pixels """
scaling = get_config().scaling_factor
size = self._sizes[self._globals.tk_faces_size.get().lower().replace(" ", "")]
size = self._sizes[self._globals.var_faces_size.get().lower().replace(" ", "")]
scaled = size * scaling
return int(round(scaled / 2) * 2)
@ -328,10 +328,11 @@ class FacesViewer(tk.Canvas): # pylint:disable=too-many-ancestors
Updates the mask type when the user changes the selected mask types
Toggles the face viewer annotations on an optional annotation button press.
"""
for var in (self._globals.tk_faces_size, self._globals.tk_filter_mode):
var.trace_add("write", lambda *e, v=var: self.refresh_grid(v))
var = detected_faces.tk_face_count_changed
var.trace_add("write", lambda *e, v=var: self.refresh_grid(v, retain_position=True))
for strvar in (self._globals.var_faces_size, self._globals.var_filter_mode):
strvar.trace_add("write", lambda *e, v=strvar: self.refresh_grid(v))
boolvar = detected_faces.tk_face_count_changed
boolvar.trace_add("write",
lambda *e, v=boolvar: self.refresh_grid(v, retain_position=True))
self._display_frame.tk_control_colors["Mesh"].trace_add(
"write", lambda *e: self._update_mesh_color())

View file

@ -83,7 +83,7 @@ class HoverBox():
is_zoomed = self._globals.is_zoomed
if (-1 in face or (frame_idx == self._globals.frame_index
and (not is_zoomed or
(is_zoomed and face_idx == self._globals.tk_face_index.get())))):
(is_zoomed and face_idx == self._globals.face_index)))):
self._clear()
self._canvas.config(cursor="")
self._current_frame_index = None
@ -125,14 +125,14 @@ class HoverBox():
if frame_id is None or (frame_id == self._globals.frame_index and not is_zoomed):
return
face_idx = self._current_face_index if is_zoomed else 0
self._globals.tk_face_index.set(face_idx)
self._globals.set_face_index(face_idx)
transport_id = self._grid.transport_index_from_frame(frame_id)
logger.trace("frame_index: %s, transport_id: %s, face_idx: %s",
frame_id, transport_id, face_idx)
if transport_id is None:
return
self._navigation.stop_playback()
self._globals.tk_transport_index.set(transport_id)
self._globals.var_transport_index.set(transport_id)
self._viewport.move_active_to_top()
self.on_hover(None)
@ -192,7 +192,7 @@ class ActiveFrame():
"edited": tk_edited_variable}
self._assets: Asset = Asset([], [], [], [])
self._globals.tk_update_active_viewport.trace_add("write",
self._globals.var_update_active_viewport.trace_add("write",
lambda *e: self._reload_callback())
tk_edited_variable.trace_add("write", lambda *e: self._update_on_edit())
logger.debug("Initialized: %s", self.__class__.__name__)
@ -205,7 +205,7 @@ class ActiveFrame():
@property
def current_frame(self) -> np.ndarray:
""" :class:`numpy.ndarray`: A BGR version of the frame currently being displayed. """
return self._globals.current_frame["image"]
return self._globals.current_frame.image
@property
def _size(self) -> int:
@ -221,7 +221,7 @@ class ActiveFrame():
def _reload_callback(self) -> None:
""" If a frame has changed, triggering the variable, then update the active frame. Return
having done nothing if the variable is resetting. """
if self._globals.tk_update_active_viewport.get():
if self._globals.var_update_active_viewport.get():
self.reload_annotations()
def reload_annotations(self) -> None:
@ -249,7 +249,7 @@ class ActiveFrame():
self._update_face()
self._canvas.tag_raise("active_highlighter")
self._globals.tk_update_active_viewport.set(False)
self._globals.var_update_active_viewport.set(False)
self._last_execution["frame_index"] = self.frame_index
def _clear_previous(self) -> None:

View file

@ -48,11 +48,11 @@ class Navigation():
frame_count = self._det_faces.filter.count
if self._current_nav_frame_count == frame_count:
logger.trace("Filtered count has not changed. Returning")
if self._globals.tk_filter_mode.get() == "Misaligned Faces":
if self._globals.var_filter_mode.get() == "Misaligned Faces":
self._det_faces.tk_face_count_changed.set(True)
self._update_total_frame_count()
if reset_progress:
self._globals.tk_transport_index.set(0)
self._globals.var_transport_index.set(0)
def _update_total_frame_count(self, *args): # pylint:disable=unused-argument
""" Update the displayed number of total frames that meet the current filter criteria.
@ -70,7 +70,7 @@ class Navigation():
logger.debug("Filtered frame count has changed. Updating from %s to %s",
self._current_nav_frame_count, frame_count)
self._nav["scale"].config(to=max_frame)
self._nav["label"].config(text="/{}".format(max_frame))
self._nav["label"].config(text=f"/{max_frame}")
state = "disabled" if max_frame == 0 else "normal"
self._nav["entry"].config(state=state)
@ -106,7 +106,7 @@ class Navigation():
logger.debug("End of Stream. Not incrementing")
self.stop_playback()
return
self._globals.tk_transport_index.set(min(position + 1, max(0, frame_count - 1)))
self._globals.var_transport_index.set(min(position + 1, max(0, frame_count - 1)))
def decrement_frame(self):
""" Update The frame navigation position to the previous frame based on filter. """
@ -116,11 +116,11 @@ class Navigation():
if not face_count_change and (self._det_faces.filter.count == 0 or position == 0):
logger.debug("End of Stream. Not decrementing")
return
self._globals.tk_transport_index.set(min(max(0, self._det_faces.filter.count - 1),
self._globals.var_transport_index.set(min(max(0, self._det_faces.filter.count - 1),
max(0, position - 1)))
def _get_safe_frame_index(self):
""" Obtain the current frame position from the tk_transport_index variable in
""" Obtain the current frame position from the var_transport_index variable in
a safe manner (i.e. handle for non-numeric)
Returns
@ -129,32 +129,32 @@ class Navigation():
The current transport frame index
"""
try:
retval = self._globals.tk_transport_index.get()
retval = self._globals.var_transport_index.get()
except tk.TclError as err:
if "expected floating-point" not in str(err):
raise
val = str(err).split(" ")[-1].replace("\"", "")
val = str(err).rsplit(" ", maxsplit=1)[-1].replace("\"", "")
retval = "".join(ch for ch in val if ch.isdigit())
retval = 0 if not retval else int(retval)
self._globals.tk_transport_index.set(retval)
self._globals.var_transport_index.set(retval)
return retval
def goto_first_frame(self):
""" Go to the first frame that meets the filter criteria. """
self.stop_playback()
position = self._globals.tk_transport_index.get()
position = self._globals.var_transport_index.get()
if position == 0:
return
self._globals.tk_transport_index.set(0)
self._globals.var_transport_index.set(0)
def goto_last_frame(self):
""" Go to the last frame that meets the filter criteria. """
self.stop_playback()
position = self._globals.tk_transport_index.get()
position = self._globals.var_transport_index.get()
frame_count = self._det_faces.filter.count
if position == frame_count - 1:
return
self._globals.tk_transport_index.set(frame_count - 1)
self._globals.var_transport_index.set(frame_count - 1)
class BackgroundImage():
@ -190,7 +190,7 @@ class BackgroundImage():
"""
self._switch_image(view_mode)
logger.trace("Updating background frame")
getattr(self, "_update_tk_{}".format(self._current_view_mode))()
getattr(self, f"_update_tk_{self._current_view_mode}")()
def _switch_image(self, view_mode):
""" Switch the image between the full frame image and the zoomed face image.
@ -206,10 +206,10 @@ class BackgroundImage():
self._zoomed_centering = self._canvas.active_editor.zoomed_centering
logger.trace("Switching background image from '%s' to '%s'",
self._current_view_mode, view_mode)
img = getattr(self, "_tk_{}".format(view_mode))
img = getattr(self, f"_tk_{view_mode}")
self._canvas.itemconfig(self._image, image=img)
self._globals.tk_is_zoomed.set(view_mode == "face")
self._globals.tk_face_index.set(0)
self._globals.set_zoomed(view_mode == "face")
self._globals.set_face_index(0)
def _update_tk_face(self):
""" Update the currently zoomed face. """
@ -239,14 +239,14 @@ class BackgroundImage():
if face_idx + 1 > faces_in_frame:
logger.debug("Resetting face index to 0 for more faces in frame than current index: ("
"faces_in_frame: %s, zoomed_face_index: %s", faces_in_frame, face_idx)
self._globals.tk_face_index.set(0)
self._globals.set_face_index(0)
if faces_in_frame == 0:
face = np.ones((size, size, 3), dtype="uint8")
else:
det_face = self._det_faces.current_faces[frame_idx][face_idx]
face = AlignedFace(det_face.landmarks_xy,
image=self._globals.current_frame["image"],
image=self._globals.current_frame.image,
centering=self._zoomed_centering,
size=size).face
logger.trace("face shape: %s", face.shape)
@ -254,9 +254,9 @@ class BackgroundImage():
def _update_tk_frame(self):
""" Place the currently held frame into :attr:`_tk_frame`. """
img = cv2.resize(self._globals.current_frame["image"],
self._globals.current_frame["display_dims"],
interpolation=self._globals.current_frame["interpolation"])[..., 2::-1]
img = cv2.resize(self._globals.current_frame.image,
self._globals.current_frame.display_dims,
interpolation=self._globals.current_frame.interpolation)[..., 2::-1]
padding = self._get_padding(img.shape[:2])
if any(padding):
img = cv2.copyMakeBorder(img, *padding, cv2.BORDER_CONSTANT)

View file

@ -41,9 +41,9 @@ class Editor():
self._globals = canvas._globals
self._det_faces = detected_faces
self._current_color = dict()
self._current_color = {}
self._actions = OrderedDict()
self._controls = dict(header=control_text, controls=[])
self._controls = {"header": control_text, "controls": []}
self._add_key_bindings(key_bindings)
self._add_actions()
@ -51,7 +51,7 @@ class Editor():
self._add_annotation_format_controls()
self._mouse_location = None
self._drag_data = dict()
self._drag_data = {}
self._drag_callback = None
self.bind_mouse_motion()
logger.debug("Initialized %s", self.__class__.__name__)
@ -80,7 +80,7 @@ class Editor():
def view_mode(self):
""" ["frame", "face"]: The view mode for the currently selected editor. If the editor does
not have a view mode that can be updated, then `"frame"` will be returned. """
tk_var = self._actions.get("magnify", dict()).get("tk_var", None)
tk_var = self._actions.get("magnify", {}).get("tk_var", None)
retval = "frame" if tk_var is None or not tk_var.get() else "face"
return retval
@ -106,7 +106,7 @@ class Editor():
@property
def _control_vars(self):
""" dict: The tk control panel variables for the currently selected editor. """
return self._canvas.control_tk_vars.get(self.__class__.__name__, dict())
return self._canvas.control_tk_vars.get(self.__class__.__name__, {})
@property
def controls(self):
@ -155,7 +155,7 @@ class Editor():
for key, method in key_bindings.items():
logger.debug("Binding key '%s' to method %s for editor '%s'",
key, method, self.__class__.__name__)
self._canvas.key_bindings.setdefault(key, dict())["bound_to"] = None
self._canvas.key_bindings.setdefault(key, {})["bound_to"] = None
self._canvas.key_bindings[key][self.__class__.__name__] = method
@staticmethod
@ -187,7 +187,7 @@ class Editor():
for cnr in bounding_box)
return display_anchors, grab_anchors
def update_annotation(self): # pylint:disable=no-self-use
def update_annotation(self):
""" Update the display annotations for the current objects.
Override for specific editors.
@ -233,7 +233,7 @@ class Editor():
"""
object_color_keys = self._get_object_color_keys(key, object_type)
tracking_id = "_".join((key, str(face_index)))
face_tag = "face_{}".format(face_index)
face_tag = f"face_{face_index}"
face_objects = set(self._canvas.find_withtag(face_tag))
annotation_objects = set(self._canvas.find_withtag(key))
existing_object = tuple(face_objects.intersection(annotation_objects))
@ -311,7 +311,7 @@ class Editor():
coordinates, object_kwargs)
object_kwargs["tags"] = self._set_object_tags(face_index, key)
item_id = getattr(self._canvas,
"create_{}".format(object_type))(*coordinates, **object_kwargs)
f"create_{object_type}")(*coordinates, **object_kwargs)
return item_id
def _set_object_tags(self, face_index, key):
@ -329,17 +329,17 @@ class Editor():
list
The generated tags for the current object
"""
tags = ["face_{}".format(face_index),
tags = [f"face_{face_index}",
self.__class__.__name__,
"{}_face_{}".format(self.__class__.__name__, face_index),
f"{self.__class__.__name__}_face_{face_index}",
key,
"{}_face_{}".format(key, face_index)]
f"{key}_face_{face_index}"]
if "_" in key:
split_key = key.split("_")
if split_key[-1].isdigit():
base_tag = "_".join(split_key[:-1])
tags.append(base_tag)
tags.append("{}_face_{}".format(base_tag, face_index))
tags.append(f"{base_tag}_face_{face_index}")
return tags
def _update_existing_object(self, item_id, coordinates, object_kwargs,
@ -366,11 +366,11 @@ class Editor():
"""
update_color = (object_color_keys and
object_kwargs[object_color_keys[0]] != self._current_color[tracking_id])
update_kwargs = dict(state=object_kwargs.get("state", "normal"))
update_kwargs = {"state": object_kwargs.get("state", "normal")}
if update_color:
for key in object_color_keys:
update_kwargs[key] = object_kwargs[object_color_keys[0]]
if self._canvas.type(item_id) == "image" and "image" in object_kwargs:
if self._canvas.type(item_id) == "image" and "image" in object_kwargs: # noqa:E721
update_kwargs["image"] = object_kwargs["image"]
logger.trace("Updating coordinates: (item_id: '%s', object_kwargs: %s, "
"coordinates: %s, update_kwargs: %s", item_id, object_kwargs,
@ -433,7 +433,7 @@ class Editor():
The tkinter mouse event. Unused but for default action, but available for editor
specific actions
"""
self._drag_data = dict()
self._drag_data = {}
self._drag_callback = None
def _drag(self, event):
@ -461,7 +461,7 @@ class Editor():
event: :class:`tkinter.Event`
The tkinter mouse event. Unused but required
"""
self._drag_data = dict()
self._drag_data = {}
def _scale_to_display(self, points):
""" Scale and offset the given points to the current display scale and offset values.
@ -476,7 +476,7 @@ class Editor():
:class:`numpy.ndarray`
The adjusted x, y co-ordinates for display purposes rounded to the nearest integer
"""
retval = np.rint((points * self._globals.current_frame["scale"])
retval = np.rint((points * self._globals.current_frame.scale)
+ self._canvas.offset).astype("int32")
logger.trace("Original points: %s, scaled points: %s", points, retval)
return retval
@ -499,7 +499,7 @@ class Editor():
integer
"""
offset = self._canvas.offset if do_offset else (0, 0)
retval = np.rint((points - offset) / self._globals.current_frame["scale"]).astype("int32")
retval = np.rint((points - offset) / self._globals.current_frame.scale).astype("int32")
logger.trace("Original points: %s, scaled points: %s", points, retval)
return retval
@ -532,7 +532,11 @@ class Editor():
Default: ``None``
"""
var = tk.BooleanVar()
action = dict(icon=icon, helptext=helptext, group=group, tk_var=var, hotkey=hotkey)
action = {"icon": icon,
"helptext": helptext,
"group": group,
"tk_var": var,
"hotkey": hotkey}
logger.debug("Adding action: %s", action)
self._actions[title] = action
@ -567,7 +571,7 @@ class Editor():
group_key = "none" if group_key == "_master" else group_key
annotation_key = option.title.replace(" ", "")
self._canvas.control_tk_vars.setdefault(
editor_key, dict()).setdefault(group_key, dict())[annotation_key] = option.tk_var
editor_key, {}).setdefault(group_key, {})[annotation_key] = option.tk_var
def _add_annotation_format_controls(self):
""" Add the annotation display (color/size) controls to :attr:`_annotation_formats`.
@ -594,7 +598,7 @@ class Editor():
default=self._default_colors[annotation_key],
helptext="Set the annotation color")
colors.set(self._default_colors[annotation_key])
self._annotation_formats.setdefault(annotation_key, dict())["color"] = colors
self._annotation_formats.setdefault(annotation_key, {})["color"] = colors
self._annotation_formats[annotation_key]["mask_opacity"] = opacity
for editor in editors:
@ -627,4 +631,6 @@ class View(Editor):
""" Add the optional action buttons to the viewer. Current actions are Zoom. """
self._add_action("magnify", "zoom", _("Magnify/Demagnify the View"),
group=None, hotkey="M")
self._actions["magnify"]["tk_var"].trace("w", lambda *e: self._globals.tk_update.set(True))
self._actions["magnify"]["tk_var"].trace_add(
"write",
lambda *e: self._globals.var_full_update.set(True))

View file

@ -105,7 +105,7 @@ class BoundingBox(Editor):
for idx, face in enumerate(self._face_iterator):
box = np.array([(face.left, face.top), (face.right, face.bottom)])
box = self._scale_to_display(box).astype("int32").flatten()
kwargs = dict(outline=color, width=1)
kwargs = {"outline": color, "width": 1}
logger.trace("frame_index: %s, face_index: %s, box: %s, kwargs: %s",
self._globals.frame_index, idx, box, kwargs)
self._object_tracker(key, "rectangle", idx, box, kwargs)
@ -137,10 +137,10 @@ class BoundingBox(Editor):
(bounding_box[2], bounding_box[3]),
(bounding_box[0], bounding_box[3])))
for idx, (anc_dsp, anc_grb) in enumerate(zip(*anchor_points)):
dsp_kwargs = dict(outline=color, fill=fill_color, width=1)
grb_kwargs = dict(outline="", fill="", width=1, activefill=activefill_color)
dsp_key = "bb_anc_dsp_{}".format(idx)
grb_key = "bb_anc_grb_{}".format(idx)
dsp_kwargs = {"outline": color, "fill": fill_color, "width": 1}
grb_kwargs = {"outline": '', "fill": '', "width": 1, "activefill": activefill_color}
dsp_key = f"bb_anc_dsp_{idx}"
grb_key = f"bb_anc_grb_{idx}"
self._object_tracker(dsp_key, "oval", face_index, anc_dsp, dsp_kwargs)
self._object_tracker(grb_key, "oval", face_index, anc_grb, grb_kwargs)
logger.trace("Updated bounding box anchor annotations")
@ -193,8 +193,9 @@ class BoundingBox(Editor):
corner_idx = int(next(tag for tag in tags
if tag.startswith("bb_anc_grb_")
and "face_" not in tag).split("_")[-1])
self._canvas.config(cursor="{}_{}_corner".format(*self._corner_order[corner_idx]))
self._mouse_location = ("anchor", "{}_{}".format(face_idx, corner_idx))
pos_x, pos_y = self._corner_order[corner_idx]
self._canvas.config(cursor=f"{pos_x}_{pos_y}_corner")
self._mouse_location = ("anchor", f"{face_idx}_{corner_idx}")
return True
def _check_cursor_bounding_box(self, event):
@ -242,7 +243,7 @@ class BoundingBox(Editor):
"""
if self._globals.frame_index == -1:
return False
display_dims = self._globals.current_frame["display_dims"]
display_dims = self._globals.current_frame.display_dims
if (self._canvas.offset[0] <= event.x <= display_dims[0] + self._canvas.offset[0] and
self._canvas.offset[1] <= event.y <= display_dims[1] + self._canvas.offset[1]):
self._canvas.config(cursor="plus")
@ -275,7 +276,7 @@ class BoundingBox(Editor):
The tkinter mouse event.
"""
if self._mouse_location is None:
self._drag_data = dict()
self._drag_data = {}
self._drag_callback = None
return
if self._mouse_location[0] == "anchor":
@ -315,7 +316,7 @@ class BoundingBox(Editor):
event: :class:`tkinter.Event`
The tkinter mouse event
"""
size = min(self._globals.current_frame["display_dims"]) // 8
size = min(self._globals.current_frame.display_dims) // 8
box = (event.x - size, event.y - size, event.x + size, event.y + size)
logger.debug("Creating new bounding box: %s ", box)
self._det_faces.update.add(self._globals.frame_index, *self._coords_to_bounding_box(box))
@ -329,7 +330,7 @@ class BoundingBox(Editor):
The tkinter mouse event.
"""
face_idx = int(self._mouse_location[1].split("_")[0])
face_tag = "bb_box_face_{}".format(face_idx)
face_tag = f"bb_box_face_{face_idx}"
box = self._canvas.coords(face_tag)
logger.trace("Face Index: %s, Corner Index: %s. Original ROI: %s",
face_idx, self._drag_data["corner"], box)
@ -361,7 +362,7 @@ class BoundingBox(Editor):
face_idx = int(self._mouse_location[1])
shift = (event.x - self._drag_data["current_location"][0],
event.y - self._drag_data["current_location"][1])
face_tag = "bb_box_face_{}".format(face_idx)
face_tag = f"bb_box_face_{face_idx}"
coords = np.array(self._canvas.coords(face_tag)) + (*shift, *shift)
logger.trace("face_tag: %s, shift: %s, new co-ords: %s", face_tag, shift, coords)
self._det_faces.update.bounding_box(self._globals.frame_index,

View file

@ -61,9 +61,9 @@ class ExtractBox(Editor):
aligned = AlignedFace(face.landmarks_xy, centering="face")
box = self._scale_to_display(aligned.original_roi).flatten()
top_left = box[:2] - 10
kwargs = dict(fill=color, font=("Default", 20, "bold"), text=str(idx))
kwargs = {"fill": color, "font": ('Default', 20, 'bold'), "text": str(idx)}
self._object_tracker("eb_text", "text", idx, top_left, kwargs)
kwargs = dict(fill="", outline=color, width=1)
kwargs = {"fill": '', "outline": color, "width": 1}
self._object_tracker("eb_box", "polygon", idx, box, kwargs)
self._update_anchor_annotation(idx, box, color)
logger.trace("Updated extract box annotations")
@ -93,10 +93,10 @@ class ExtractBox(Editor):
extract_box[4:6],
extract_box[6:]))
for idx, (anc_dsp, anc_grb) in enumerate(zip(*anchor_points)):
dsp_kwargs = dict(outline=color, fill=fill_color, width=1)
grb_kwargs = dict(outline="", fill="", width=1, activefill=activefill_color)
dsp_key = "eb_anc_dsp_{}".format(idx)
grb_key = "eb_anc_grb_{}".format(idx)
dsp_kwargs = {"outline": color, "fill": fill_color, "width": 1}
grb_kwargs = {"outline": '', "fill": '', "width": 1, "activefill": activefill_color}
dsp_key = f"eb_anc_dsp_{idx}"
grb_key = f"eb_anc_grb_{idx}"
self._object_tracker(dsp_key, "oval", face_index, anc_dsp, dsp_kwargs)
self._object_tracker(grb_key, "oval", face_index, anc_grb, grb_kwargs)
logger.trace("Updated extract box anchor annotations")
@ -143,7 +143,8 @@ class ExtractBox(Editor):
if tag.startswith("eb_anc_grb_")
and "face_" not in tag).split("_")[-1])
self._canvas.config(cursor="{}_{}_corner".format(*self._corner_order[corner_idx]))
pos_x, pos_y = self._corner_order[corner_idx]
self._canvas.config(cursor=f"{pos_x}_{pos_y}_corner")
self._mouse_location = ("anchor", face_idx, corner_idx)
return True
@ -222,11 +223,11 @@ class ExtractBox(Editor):
The tkinter mouse event.
"""
if self._mouse_location is None:
self._drag_data = dict()
self._drag_data = {}
self._drag_callback = None
return
self._drag_data["current_location"] = np.array((event.x, event.y))
callback = dict(anchor=self._resize, rotate=self._rotate, box=self._move)
callback = {"anchor": self._resize, "rotate": self._rotate, "box": self._move}
self._drag_callback = callback[self._mouse_location[0]]
def _drag_stop(self, event): # pylint:disable=unused-argument
@ -270,7 +271,7 @@ class ExtractBox(Editor):
The tkinter mouse event.
"""
face_idx = self._mouse_location[1]
face_tag = "eb_box_face_{}".format(face_idx)
face_tag = f"eb_box_face_{face_idx}"
position = np.array((event.x, event.y))
box = np.array(self._canvas.coords(face_tag))
center = np.array((sum(box[0::2]) / 4, sum(box[1::2]) / 4))
@ -365,7 +366,7 @@ class ExtractBox(Editor):
The tkinter mouse event.
"""
face_idx = self._mouse_location[1]
face_tag = "eb_box_face_{}".format(face_idx)
face_tag = f"eb_box_face_{face_idx}"
box = np.array(self._canvas.coords(face_tag))
position = np.array((event.x, event.y))

View file

@ -36,7 +36,7 @@ class Landmarks(Editor):
super().__init__(canvas, detected_faces, control_text)
# Clear selection box on an editor or frame change
self._canvas._tk_action_var.trace("w", lambda *e: self._reset_selection())
self._globals.tk_frame_index.trace("w", lambda *e: self._reset_selection())
self._globals.var_frame_index.trace_add("write", lambda *e: self._reset_selection())
def _add_actions(self):
""" Add the optional action buttons to the viewer. Current actions are Point, Select
@ -55,7 +55,7 @@ class Landmarks(Editor):
tkinter callback arguments. Required but unused.
"""
self._reset_selection()
self._globals.tk_update.set(True)
self._globals.var_full_update.set(True)
def _reset_selection(self, event=None): # pylint:disable=unused-argument
""" Reset the selection box and the selected landmark annotations. """

View file

@ -82,7 +82,9 @@ class Mask(Editor):
group=None, hotkey="M")
self._add_action("draw", "draw", _("Draw Tool"), group="paint", hotkey="D")
self._add_action("erase", "erase", _("Erase Tool"), group="paint", hotkey="E")
self._actions["magnify"]["tk_var"].trace("w", lambda *e: self._globals.tk_update.set(True))
self._actions["magnify"]["tk_var"].trace(
"w",
lambda *e: self._globals.var_full_update.set(True))
def _add_controls(self):
""" Add the mask specific control panel controls.
@ -143,21 +145,21 @@ class Mask(Editor):
mask_type = self._control_vars["display"]["MaskType"].get()
if mask_type == self._mask_type:
return
self._meta = dict(position=self._globals.frame_index)
self._meta = {"position": self._globals.frame_index}
self._mask_type = mask_type
self._globals.tk_update.set(True)
self._globals.var_full_update.set(True)
def hide_annotation(self, tag=None):
""" Clear the mask :attr:`_meta` dict when hiding the annotation. """
super().hide_annotation()
self._meta = dict()
self._meta = {}
def update_annotation(self):
""" Update the mask annotation with the latest mask. """
position = self._globals.frame_index
if position != self._meta.get("position", -1):
# Reset meta information when moving to a new frame
self._meta = dict(position=position)
self._meta = {"position": position}
key = self.__class__.__name__
mask_type = self._control_vars["display"]["MaskType"].get().lower()
color = self._control_color[1:]
@ -221,21 +223,21 @@ class Mask(Editor):
- slices: The (`x`, `y`) slice objects required to extract the mask ROI
from the full frame
"""
frame_dims = self._globals.current_frame["display_dims"]
frame_dims = self._globals.current_frame.display_dims
scaled_mask_roi = np.rint(mask.original_roi *
self._globals.current_frame["scale"]).astype("int32")
self._globals.current_frame.scale).astype("int32")
# Scale and clip the ROI to fit within display frame boundaries
clipped_roi = scaled_mask_roi.clip(min=(0, 0), max=frame_dims)
# Obtain min and max points to get ROI as a rectangle
min_max = dict(min=clipped_roi.min(axis=0), max=clipped_roi.max(axis=0))
min_max = {"min": clipped_roi.min(axis=0), "max": clipped_roi.max(axis=0)}
# Create a bounding box rectangle ROI
roi_dims = np.rint((min_max["max"][1] - min_max["min"][1],
min_max["max"][0] - min_max["min"][0])).astype("uint16")
roi = dict(mask=np.zeros(roi_dims, dtype="uint8")[..., None],
corners=np.expand_dims(scaled_mask_roi - min_max["min"], axis=0))
roi = {"mask": np.zeros(roi_dims, dtype="uint8")[..., None],
"corners": np.expand_dims(scaled_mask_roi - min_max["min"], axis=0)}
# Block out areas outside of the actual mask ROI polygon
cv2.fillPoly(roi["mask"], roi["corners"], 255)
logger.trace("Setting Full Frame mask ROI. shape: %s", roi["mask"].shape)
@ -246,8 +248,8 @@ class Mask(Editor):
# Adjust affine matrix for internal mask size and display dimensions
adjustments = (np.array([[mask_scale, 0., 0.], [0., mask_scale, 0.]]),
np.array([[1 / self._globals.current_frame["scale"], 0., 0.],
[0., 1 / self._globals.current_frame["scale"], 0.],
np.array([[1 / self._globals.current_frame.scale, 0., 0.],
[0., 1 / self._globals.current_frame.scale, 0.],
[0., 0., 1.]]))
in_matrix = np.dot(adjustments[0],
np.concatenate((mask.affine_matrix, np.array([[0., 0., 1.]]))))
@ -285,7 +287,7 @@ class Mask(Editor):
top_left = self._zoomed_roi[:2]
# Hide all masks and only display selected
self._canvas.itemconfig("Mask", state="hidden")
self._canvas.itemconfig("Mask_face_{}".format(face_index), state="normal")
self._canvas.itemconfig(f"Mask_face_{face_index}", state="normal")
else:
display_image = self._update_mask_image_full_frame(mask, rgb_color, face_index)
top_left = self._meta["top_left"][face_index]
@ -305,7 +307,7 @@ class Mask(Editor):
"image",
face_index,
top_left,
dict(image=self._tk_faces[face_index], anchor=tk.NW))
{"image": self._tk_faces[face_index], "anchor": tk.NW})
def _update_mask_image_zoomed(self, mask, rgb_color):
""" Update the mask image when zoomed in.
@ -346,7 +348,7 @@ class Mask(Editor):
:class: `PIL.Image`
The full frame mask image formatted for display
"""
frame_dims = self._globals.current_frame["display_dims"]
frame_dims = self._globals.current_frame.display_dims
frame = np.zeros(frame_dims + (1, ), dtype="uint8")
interpolator = self._meta["interpolator"][face_index]
slices = self._meta["slices"][face_index]
@ -377,13 +379,13 @@ class Mask(Editor):
else:
box = self._scale_to_display(mask.original_roi).flatten()
top_left = box[:2] - 10
kwargs = dict(fill=color, font=("Default", 20, "bold"), text=str(face_index))
kwargs = {"fill": color, "font": ("Default", 20, "bold"), "text": str(face_index)}
self._object_tracker("mask_text", "text", face_index, top_left, kwargs)
kwargs = dict(fill="", outline=color, width=1)
kwargs = {"fill": "", "outline": color, "width": 1}
self._object_tracker("mask_roi", "polygon", face_index, box, kwargs)
if self._globals.is_zoomed:
# Raise box above zoomed image
self._canvas.tag_raise("mask_roi_face_{}".format(face_index))
self._canvas.tag_raise(f"mask_roi_face_{face_index}")
# << MOUSE HANDLING >>
# Mouse cursor display
@ -450,7 +452,7 @@ class Mask(Editor):
"""
face_idx = self._mouse_location[1]
if face_idx is None:
self._drag_data = dict()
self._drag_data = {}
self._drag_callback = None
else:
self._drag_data["starting_location"] = np.array((event.x, event.y))
@ -532,7 +534,7 @@ class Mask(Editor):
if np.array_equal(self._drag_data["starting_location"], location[0]):
self._get_cursor_shape_mark(self._meta["mask"][face_idx], location, face_idx)
self._mask_to_alignments(face_idx)
self._drag_data = dict()
self._drag_data = {}
self._update_cursor(event)
def _get_cursor_shape_mark(self, img, location, face_idx):
@ -562,11 +564,10 @@ class Mask(Editor):
else:
cv2.circle(img, tuple(points), radius, color, thickness=-1)
def _get_cursor_shape(self, x1=0, y1=0, x2=0, y2=0, outline="black", state="hidden"):
def _get_cursor_shape(self, x_1=0, y_1=0, x_2=0, y_2=0, outline="black", state="hidden"):
if self._cursor_shape_name == "Rectangle":
return self._canvas.create_rectangle(x1, y1, x2, y2, outline=outline, state=state)
else:
return self._canvas.create_oval(x1, y1, x2, y2, outline=outline, state=state)
return self._canvas.create_rectangle(x_1, y_1, x_2, y_2, outline=outline, state=state)
return self._canvas.create_oval(x_1, y_1, x_2, y_2, outline=outline, state=state)
def _mask_to_alignments(self, face_index):
""" Update the annotated mask to alignments.

View file

@ -146,41 +146,41 @@ class DisplayFrame(ttk.Frame): # pylint:disable=too-many-ancestors
lbl_frame.pack(side=tk.RIGHT)
tbox = ttk.Entry(lbl_frame,
width=7,
textvariable=self._globals.tk_transport_index,
textvariable=self._globals.var_transport_index,
justify=tk.RIGHT)
tbox.pack(padx=0, side=tk.LEFT)
lbl = ttk.Label(lbl_frame, text=f"/{max_frame}")
lbl.pack(side=tk.RIGHT)
cmd = partial(set_slider_rounding,
var=self._globals.tk_transport_index,
var=self._globals.var_transport_index,
d_type=int,
round_to=1,
min_max=(0, max_frame))
nav = ttk.Scale(frame,
variable=self._globals.tk_transport_index,
variable=self._globals.var_transport_index,
from_=0,
to=max_frame,
command=cmd)
nav.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
self._globals.tk_transport_index.trace("w", self._set_frame_index)
self._globals.var_transport_index.trace_add("write", self._set_frame_index)
return {"entry": tbox, "scale": nav, "label": lbl}
def _set_frame_index(self, *args): # pylint:disable=unused-argument
""" Set the actual frame index based on current slider position and filter mode. """
try:
slider_position = self._globals.tk_transport_index.get()
slider_position = self._globals.var_transport_index.get()
except TclError:
# don't update the slider when the entry box has been cleared of any value
return
frames = self._det_faces.filter.frames_list
actual_position = max(0, min(len(frames) - 1, slider_position))
if actual_position != slider_position:
self._globals.tk_transport_index.set(actual_position)
self._globals.var_transport_index.set(actual_position)
frame_idx = frames[actual_position] if frames else -1
logger.trace("slider_position: %s, frame_idx: %s", actual_position, frame_idx)
self._globals.tk_frame_index.set(frame_idx)
self._globals.var_frame_index.set(frame_idx)
def _add_transport(self):
""" Add video transport controls """
@ -237,14 +237,14 @@ class DisplayFrame(ttk.Frame): # pylint:disable=too-many-ancestors
frame: :class:`tkinter.ttk.Frame`
The Filter Frame that holds the filter combo box
"""
self._globals.tk_filter_mode.set("All Frames")
self._globals.tk_filter_mode.trace("w", self._navigation.nav_scale_callback)
self._globals.var_filter_mode.set("All Frames")
self._globals.var_filter_mode.trace("w", self._navigation.nav_scale_callback)
nav_frame = ttk.Frame(frame)
lbl = ttk.Label(nav_frame, text="Filter:")
lbl.pack(side=tk.LEFT, padx=(0, 5))
combo = ttk.Combobox(
nav_frame,
textvariable=self._globals.tk_filter_mode,
textvariable=self._globals.var_filter_mode,
state="readonly",
values=self._filter_modes)
combo.pack(side=tk.RIGHT)
@ -260,7 +260,7 @@ class DisplayFrame(ttk.Frame): # pylint:disable=too-many-ancestors
The Filter Frame that holds the filter threshold slider
"""
slider_frame = ttk.Frame(frame)
tk_var = self._globals.tk_filter_distance
tk_var = self._globals.var_filter_distance
min_max = (5, 20)
ctl_frame = ttk.Frame(slider_frame)
@ -284,22 +284,22 @@ class DisplayFrame(ttk.Frame): # pylint:disable=too-many-ancestors
Tooltip(item,
text=self._helptext["distance"],
wrap_length=200)
tk_var.trace("w", self._navigation.nav_scale_callback)
tk_var.trace_add("write", self._navigation.nav_scale_callback)
self._optional_widgets["distance_slider"] = slider_frame
def pack_threshold_slider(self):
""" Display or hide the threshold slider depending on the current filter mode. For
misaligned faces filter, display the slider. Hide for all other filters. """
if self._globals.tk_filter_mode.get() == "Misaligned Faces":
if self._globals.var_filter_mode.get() == "Misaligned Faces":
self._optional_widgets["distance_slider"].pack(side=tk.LEFT)
else:
self._optional_widgets["distance_slider"].pack_forget()
def cycle_filter_mode(self):
""" Cycle the navigation mode combo entry """
current_mode = self._globals.filter_mode
current_mode = self._globals.var_filter_mode.get()
idx = (self._filter_modes.index(current_mode) + 1) % len(self._filter_modes)
self._globals.tk_filter_mode.set(self._filter_modes[idx])
self._globals.var_filter_mode.set(self._filter_modes[idx])
def set_action(self, key):
""" Set the current action based on keyboard shortcut
@ -318,7 +318,7 @@ class DisplayFrame(ttk.Frame): # pylint:disable=too-many-ancestors
framesize = (event.width, event.height)
logger.trace("Resizing video frame. Framesize: %s", framesize)
self._globals.set_frame_display_dims(*framesize)
self._globals.tk_update.set(True)
self._globals.var_full_update.set(True)
# << TRANSPORT >> #
def _play(self, *args, frame_count=None): # pylint:disable=unused-argument
@ -475,17 +475,16 @@ class ActionsFrame(ttk.Frame): # pylint:disable=too-many-ancestors
sep = ttk.Frame(frame, height=2, relief=tk.RIDGE)
sep.pack(fill=tk.X, pady=5, side=tk.TOP)
buttons = {}
tk_frame_index = self._globals.tk_frame_index
for action in ("copy_prev", "copy_next", "reload"):
if action == "reload":
icon = "reload3"
cmd = lambda f=tk_frame_index: self._det_faces.revert_to_saved(f.get()) # noqa:E731 # pylint:disable=line-too-long,unnecessary-lambda-assignment
cmd = lambda f=self._globals: self._det_faces.revert_to_saved(f.frame_index) # noqa:E731,E501 # pylint:disable=line-too-long,unnecessary-lambda-assignment
helptext = _("Revert to saved Alignments ({})").format(lookup[action][1])
else:
icon = action
direction = action.replace("copy_", "")
cmd = lambda f=tk_frame_index, d=direction: self._det_faces.update.copy( # noqa:E731 # pylint:disable=line-too-long,unnecessary-lambda-assignment
f.get(), d)
cmd = lambda f=self._globals, d=direction: self._det_faces.update.copy( # noqa:E731,E501 # pylint:disable=line-too-long,unnecessary-lambda-assignment
f.frame_index, d)
helptext = _("Copy {} Alignments ({})").format(*lookup[action])
state = ["!disabled"] if action == "copy_next" else ["disabled"]
button = ttk.Button(frame,
@ -496,8 +495,8 @@ class ActionsFrame(ttk.Frame): # pylint:disable=too-many-ancestors
button.pack()
Tooltip(button, text=helptext)
buttons[action] = button
self._globals.tk_frame_index.trace("w", self._disable_enable_copy_buttons)
self._globals.tk_update.trace("w", self._disable_enable_reload_button)
self._globals.var_frame_index.trace_add("write", self._disable_enable_copy_buttons)
self._globals.var_full_update.trace_add("write", self._disable_enable_reload_button)
return buttons
def _disable_enable_copy_buttons(self, *args): # pylint:disable=unused-argument
@ -707,7 +706,7 @@ class FrameViewer(tk.Canvas): # pylint:disable=too-many-ancestors
def offset(self):
""" tuple: The (`width`, `height`) offset of the canvas based on the size of the currently
displayed image """
frame_dims = self._globals.current_frame["display_dims"]
frame_dims = self._globals.current_frame.display_dims
offset_x = (self._globals.frame_display_dims[0] - frame_dims[0]) / 2
offset_y = (self._globals.frame_display_dims[1] - frame_dims[1]) / 2
logger.trace("offset_x: %s, offset_y: %s", offset_x, offset_y)
@ -733,11 +732,11 @@ class FrameViewer(tk.Canvas): # pylint:disable=too-many-ancestors
""" Add the callback trace functions to the :class:`tkinter.Variable` s
Adds callbacks for:
:attr:`_globals.tk_update` Update the display for the current image
:attr:`_globals.var_full_update` Update the display for the current image
:attr:`__tk_action_var` Update the mouse display tracking for current action
"""
self._globals.tk_update.trace("w", self._update_display)
self._tk_action_var.trace("w", self._change_active_editor)
self._globals.var_full_update.trace_add("write", self._update_display)
self._tk_action_var.trace_add("write", self._change_active_editor)
def _change_active_editor(self, *args): # pylint:disable=unused-argument
""" Update the display for the active editor.
@ -757,7 +756,7 @@ class FrameViewer(tk.Canvas): # pylint:disable=too-many-ancestors
self.active_editor.bind_mouse_motion()
self.active_editor.set_mouse_click_actions()
self._globals.tk_update.set(True)
self._globals.var_full_update.set(True)
def _update_display(self, *args): # pylint:disable=unused-argument
""" Update the display on frame cache update
@ -767,7 +766,7 @@ class FrameViewer(tk.Canvas): # pylint:disable=too-many-ancestors
A little hacky, but the editors to display or hide are processed in alphabetical
order, so that they are always processed in the same order (for tag lowering and raising)
"""
if not self._globals.tk_update.get():
if not self._globals.var_full_update.get():
return
zoomed_centering = self.active_editor.zoomed_centering
self._image.refresh(self.active_editor.view_mode)
@ -779,7 +778,7 @@ class FrameViewer(tk.Canvas): # pylint:disable=too-many-ancestors
if zoomed_centering != self.active_editor.zoomed_centering:
# Refresh the image if editor annotation has changed the zoom centering of the image
self._image.refresh(self.active_editor.view_mode)
self._globals.tk_update.set(False)
self._globals.var_full_update.set(False)
self.update_idletasks()
def _hide_additional_faces(self):

309
tools/manual/globals.py Normal file
View file

@ -0,0 +1,309 @@
#!/usr/bin/env python3
""" Holds global tkinter variables and information pertaining to the entire Manual tool """
from __future__ import annotations
import logging
import os
import sys
import tkinter as tk
from dataclasses import dataclass, field
import cv2
import numpy as np
from lib.gui.utils import get_config
from lib.logger import parse_class_init
from lib.utils import VIDEO_EXTENSIONS
logger = logging.getLogger(__name__)
@dataclass
class CurrentFrame:
""" Dataclass for holding information about the currently displayed frame """
image: np.ndarray = field(default_factory=lambda: np.zeros(1))
""":class:`numpy.ndarry`: The currently displayed frame in original dimensions """
scale: float = 1.0
"""float: The scaling factor to use to resize the image to the display window """
interpolation: int = cv2.INTER_AREA
"""int: The opencv interpolator ID to use for resizing the image to the display window """
display_dims: tuple[int, int] = (0, 0)
"""tuple[int, int]`: The size of the currently displayed frame, in the display window """
filename: str = ""
"""str: The filename of the currently displayed frame """
def __repr__(self) -> str:
""" Clean string representation showing numpy arrays as shape and dtype
Returns
-------
str
Loggable representation of the dataclass
"""
properties = [f"{k}={(v.shape, v.dtype) if isinstance(v, np.ndarray) else v}"
for k, v in self.__dict__.items()]
return f"{self.__class__.__name__} ({', '.join(properties)}"
@dataclass
class TKVars:
""" Holds the global TK Variables """
frame_index: tk.IntVar
""":class:`tkinter.IntVar`: The absolute frame index of the currently displayed frame"""
transport_index: tk.IntVar
""":class:`tkinter.IntVar`: The transport index of the currently displayed frame when filters
have been applied """
face_index: tk.IntVar
""":class:`tkinter.IntVar`: The face index of the currently selected face"""
filter_distance: tk.IntVar
""":class:`tkinter.IntVar`: The amount to filter by distance"""
update: tk.BooleanVar
""":class:`tkinter.BooleanVar`: Whether an update has been performed """
update_active_viewport: tk.BooleanVar
""":class:`tkinter.BooleanVar`: Whether the viewport needs updating """
is_zoomed: tk.BooleanVar
""":class:`tkinter.BooleanVar`: Whether the main window is zoomed in to a face or out to a
full frame"""
filter_mode: tk.StringVar
""":class:`tkinter.StringVar`: The currently selected filter mode """
faces_size: tk.StringVar
""":class:`tkinter.StringVar`: The pixel size of faces in the viewport """
def __repr__(self) -> str:
""" Clean string representation showing variable type as well as their value
Returns
-------
str
Loggable representation of the dataclass
"""
properties = [f"{k}={v.__class__.__name__}({v.get()})" for k, v in self.__dict__.items()]
return f"{self.__class__.__name__} ({', '.join(properties)}"
class TkGlobals():
""" Holds Tkinter Variables and other frame information that need to be accessible from all
areas of the GUI.
Parameters
----------
input_location: str
The location of the input folder of frames or video file
"""
def __init__(self, input_location: str) -> None:
logger.debug(parse_class_init(locals()))
self._tk_vars = self._get_tk_vars()
self._is_video = self._check_input(input_location)
self._frame_count = 0 # set by FrameLoader
self._frame_display_dims = (int(round(896 * get_config().scaling_factor)),
int(round(504 * get_config().scaling_factor)))
self._current_frame = CurrentFrame()
logger.debug("Initialized %s", self.__class__.__name__)
@classmethod
def _get_tk_vars(cls) -> TKVars:
""" Create and initialize the tkinter variables.
Returns
-------
:class:`TKVars`
The global tkinter variables
"""
retval = TKVars(frame_index=tk.IntVar(value=0),
transport_index=tk.IntVar(value=0),
face_index=tk.IntVar(value=0),
filter_distance=tk.IntVar(value=10),
update=tk.BooleanVar(value=False),
update_active_viewport=tk.BooleanVar(value=False),
is_zoomed=tk.BooleanVar(value=False),
filter_mode=tk.StringVar(),
faces_size=tk.StringVar())
logger.debug(retval)
return retval
@property
def current_frame(self) -> CurrentFrame:
""" :class:`CurrentFrame`: The currently displayed frame in the frame viewer with it's
meta information. """
return self._current_frame
@property
def frame_count(self) -> int:
""" int: The total number of frames for the input location """
return self._frame_count
@property
def frame_display_dims(self) -> tuple[int, int]:
""" tuple: The (`width`, `height`) of the video display frame in pixels. """
return self._frame_display_dims
@property
def is_video(self) -> bool:
""" bool: ``True`` if the input is a video file, ``False`` if it is a folder of images. """
return self._is_video
# TK Variables that need to be exposed
@property
def var_full_update(self) -> tk.BooleanVar:
""" :class:`tkinter.BooleanVar`: Flag to indicate that whole GUI should be refreshed """
return self._tk_vars.update
@property
def var_transport_index(self) -> tk.IntVar:
""" :class:`tkinter.IntVar`: The current index of the display frame's transport slider. """
return self._tk_vars.transport_index
@property
def var_frame_index(self) -> tk.IntVar:
""" :class:`tkinter.IntVar`: The current absolute frame index of the currently
displayed frame. """
return self._tk_vars.frame_index
@property
def var_filter_distance(self) -> tk.IntVar:
""" :class:`tkinter.IntVar`: The variable holding the currently selected threshold
distance for misaligned filter mode. """
return self._tk_vars.filter_distance
@property
def var_filter_mode(self) -> tk.StringVar:
""" :class:`tkinter.StringVar`: The variable holding the currently selected navigation
filter mode. """
return self._tk_vars.filter_mode
@property
def var_faces_size(self) -> tk.StringVar:
""" :class:`tkinter..IntVar`: The variable holding the currently selected Faces Viewer
thumbnail size. """
return self._tk_vars.faces_size
@property
def var_update_active_viewport(self) -> tk.BooleanVar:
""" :class:`tkinter.BooleanVar`: Boolean Variable that is traced by the viewport's active
frame to update. """
return self._tk_vars.update_active_viewport
# Raw values returned from TK Variables
@property
def face_index(self) -> int:
""" int: The currently displayed face index when in zoomed mode. """
return self._tk_vars.face_index.get()
@property
def frame_index(self) -> int:
""" int: The currently displayed frame index. NB This returns -1 if there are no frames
that meet the currently selected filter criteria. """
return self._tk_vars.frame_index.get()
@property
def is_zoomed(self) -> bool:
""" bool: ``True`` if the frame viewer is zoomed into a face, ``False`` if the frame viewer
is displaying a full frame. """
return self._tk_vars.is_zoomed.get()
@staticmethod
def _check_input(frames_location: str) -> bool:
""" Check whether the input is a video
Parameters
----------
frames_location: str
The input location for video or images
Returns
-------
bool: 'True' if input is a video 'False' if it is a folder.
"""
if os.path.isdir(frames_location):
retval = False
elif os.path.splitext(frames_location)[1].lower() in VIDEO_EXTENSIONS:
retval = True
else:
logger.error("The input location '%s' is not valid", frames_location)
sys.exit(1)
logger.debug("Input '%s' is_video: %s", frames_location, retval)
return retval
def set_face_index(self, index: int) -> None:
""" Set the currently selected face index
Parameters
----------
index: int
The currently selected face index
"""
logger.trace("Setting face index from %s to %s", # type:ignore[attr-defined]
self.face_index, index)
self._tk_vars.face_index.set(index)
def set_frame_count(self, count: int) -> None:
""" Set the count of total number of frames to :attr:`frame_count` when the
:class:`FramesLoader` has completed loading.
Parameters
----------
count: int
The number of frames that exist for this session
"""
logger.debug("Setting frame_count to : %s", count)
self._frame_count = count
def set_current_frame(self, image: np.ndarray, filename: str) -> None:
""" Set the frame and meta information for the currently displayed frame. Populates the
attribute :attr:`current_frame`
Parameters
----------
image: :class:`numpy.ndarray`
The image used to display in the Frame Viewer
filename: str
The filename of the current frame
"""
scale = min(self.frame_display_dims[0] / image.shape[1],
self.frame_display_dims[1] / image.shape[0])
self._current_frame.image = image
self._current_frame.filename = filename
self._current_frame.scale = scale
self._current_frame.interpolation = cv2.INTER_CUBIC if scale > 1.0 else cv2.INTER_AREA
self._current_frame.display_dims = (int(round(image.shape[1] * scale)),
int(round(image.shape[0] * scale)))
logger.trace(self._current_frame) # type:ignore[attr-defined]
def set_frame_display_dims(self, width: int, height: int) -> None:
""" Set the size, in pixels, of the video frame display window and resize the displayed
frame.
Used on a frame resize callback, sets the :attr:frame_display_dims`.
Parameters
----------
width: int
The width of the frame holding the video canvas in pixels
height: int
The height of the frame holding the video canvas in pixels
"""
self._frame_display_dims = (int(width), int(height))
image = self._current_frame.image
scale = min(self.frame_display_dims[0] / image.shape[1],
self.frame_display_dims[1] / image.shape[0])
self._current_frame.scale = scale
self._current_frame.interpolation = cv2.INTER_CUBIC if scale > 1.0 else cv2.INTER_AREA
self._current_frame.display_dims = (int(round(image.shape[1] * scale)),
int(round(image.shape[0] * scale)))
logger.trace(self._current_frame) # type:ignore[attr-defined]
def set_zoomed(self, state: bool) -> None:
""" Set the current zoom state
Parameters
----------
state: bool
``True`` for zoomed ``False`` for full frame
"""
logger.trace("Setting zoom state from %s to %s", # type:ignore[attr-defined]
self.is_zoomed, state)
self._tk_vars.is_zoomed.set(state)

View file

@ -1,6 +1,5 @@
#!/usr/bin/env python3
""" The Manual Tool is a tkinter driven GUI app for editing alignments files with visual tools.
This module is the main entry point into the Manual Tool. """
""" Main entry point for the Manual Tool. A GUI app for editing alignments files """
from __future__ import annotations
import logging
@ -9,24 +8,27 @@ import sys
import typing as T
import tkinter as tk
from tkinter import ttk
from dataclasses import dataclass
from time import sleep
import cv2
import numpy as np
from lib.gui.control_helper import ControlPanel
from lib.gui.utils import get_images, get_config, initialize_config, initialize_images
from lib.image import SingleFrameLoader, read_image_meta
from lib.logger import parse_class_init
from lib.multithreading import MultiThread
from lib.utils import handle_deprecated_cliopts, VIDEO_EXTENSIONS
from lib.utils import handle_deprecated_cliopts
from plugins.extract import ExtractMedia, Extractor
from .detected_faces import DetectedFaces
from .faceviewer.frame import FacesFrame
from .frameviewer.frame import DisplayFrame
from .globals import TkGlobals
from .thumbnails import ThumbsCreator
if T.TYPE_CHECKING:
from argparse import Namespace
from lib.align import DetectedFace, Mask
from lib.queue_manager import EventQueue
@ -35,6 +37,17 @@ logger = logging.getLogger(__name__)
TypeManualExtractor = T.Literal["FAN", "cv2-dnn", "mask"]
@dataclass
class _Containers:
""" Dataclass for holding the main area containers in the GUI """
main: ttk.PanedWindow
""":class:`tkinter.ttk.PanedWindow`: The main window holding the full GUI """
top: ttk.Frame
""":class:`tkinter.ttk.Frame: The top part (frame viewer) of the GUI"""
bottom: ttk.Frame
""":class:`tkinter.ttk.Frame: The bottom part (face viewer) of the GUI"""
class Manual(tk.Tk):
""" The main entry point for Faceswap's Manual Editor Tool. This tool is part of the Faceswap
Tools suite and should be called from ``python tools.py manual`` command.
@ -48,8 +61,8 @@ class Manual(tk.Tk):
The :mod:`argparse` arguments as passed in from :mod:`tools.py`
"""
def __init__(self, arguments):
logger.debug("Initializing %s: (arguments: '%s')", self.__class__.__name__, arguments)
def __init__(self, arguments: Namespace) -> None:
logger.debug(parse_class_init(locals()))
super().__init__()
arguments = handle_deprecated_cliopts(arguments)
self._validate_non_faces(arguments.frames)
@ -66,24 +79,27 @@ class Manual(tk.Tk):
video_meta_data = self._detected_faces.video_meta_data
valid_meta = all(val is not None for val in video_meta_data.values())
loader = FrameLoader(self._globals, arguments.frames, video_meta_data)
loader = FrameLoader(self._globals,
arguments.frames,
video_meta_data,
self._detected_faces.frame_list)
if valid_meta: # Load the faces whilst other threads complete if we have valid meta data
self._detected_faces.load_faces()
self._containers = self._create_containers()
self._wait_for_threads(extractor, loader, valid_meta)
if not valid_meta:
# Load the faces after other threads complete if meta data required updating
if not valid_meta: # If meta data needs updating, load faces after other threads
self._detected_faces.load_faces()
self._generate_thumbs(arguments.frames, arguments.thumb_regen, arguments.single_process)
self._display = DisplayFrame(self._containers["top"],
self._display = DisplayFrame(self._containers.top,
self._globals,
self._detected_faces)
_Options(self._containers["top"], self._globals, self._display)
_Options(self._containers.top, self._globals, self._display)
self._faces_frame = FacesFrame(self._containers["bottom"],
self._faces_frame = FacesFrame(self._containers.bottom,
self._globals,
self._detected_faces,
self._display)
@ -94,7 +110,7 @@ class Manual(tk.Tk):
logger.debug("Initialized %s", self.__class__.__name__)
@classmethod
def _validate_non_faces(cls, frames_folder):
def _validate_non_faces(cls, frames_folder: str) -> None:
""" Quick check on the input to make sure that a folder of extracted faces is not being
passed in. """
if not os.path.isdir(frames_folder):
@ -117,7 +133,7 @@ class Manual(tk.Tk):
sys.exit(1)
logger.debug("Test input file '%s' does not contain Faceswap header data", test_file)
def _wait_for_threads(self, extractor, loader, valid_meta):
def _wait_for_threads(self, extractor: Aligner, loader: FrameLoader, valid_meta: bool) -> None:
""" The :class:`Aligner` and :class:`FramesLoader` are launched in background threads.
Wait for them to be initialized prior to proceeding.
@ -150,9 +166,10 @@ class Manual(tk.Tk):
extractor.link_faces(self._detected_faces)
if not valid_meta:
logger.debug("Saving video meta data to alignments file")
self._detected_faces.save_video_meta_data(**loader.video_meta_data)
self._detected_faces.save_video_meta_data(
**loader.video_meta_data) # type:ignore[arg-type]
def _generate_thumbs(self, input_location, force, single_process):
def _generate_thumbs(self, input_location: str, force: bool, single_process: bool) -> None:
""" Check whether thumbnails are stored in the alignments file and if not generate them.
Parameters
@ -173,7 +190,7 @@ class Manual(tk.Tk):
thumbs.generate_cache()
logger.debug("Generated thumbnails cache")
def _initialize_tkinter(self):
def _initialize_tkinter(self) -> None:
""" Initialize a standalone tkinter instance. """
logger.debug("Initializing tkinter")
for widget in ("TButton", "TCheckbutton", "TRadiobutton"):
@ -184,15 +201,16 @@ class Manual(tk.Tk):
self.title("Faceswap.py - Visual Alignments")
logger.debug("Initialized tkinter")
def _create_containers(self):
def _create_containers(self) -> _Containers:
""" Create the paned window containers for various GUI elements
Returns
-------
dict:
:class:`_Containers`:
The main containers of the manual tool.
"""
logger.debug("Creating containers")
main = ttk.PanedWindow(self,
orient=tk.VERTICAL,
name="pw_main")
@ -203,11 +221,13 @@ class Manual(tk.Tk):
bottom = ttk.Frame(main, name="frame_bottom")
main.add(bottom)
retval = {"main": main, "top": top, "bottom": bottom}
retval = _Containers(main=main, top=top, bottom=bottom)
logger.debug("Created containers: %s", retval)
return retval
def _handle_key_press(self, event):
def _handle_key_press(self, event: tk.Event) -> None:
""" Keyboard shortcuts
Parameters
@ -226,7 +246,7 @@ class Manual(tk.Tk):
modifiers = {0x0001: 'shift',
0x0004: 'ctrl'}
tk_pos = self._globals.tk_frame_index
globs = self._globals
bindings = {
"z": self._display.navigation.decrement_frame,
"x": self._display.navigation.increment_frame,
@ -245,21 +265,23 @@ class Manual(tk.Tk):
"f5": lambda k=event.keysym: self._display.set_action(k),
"f9": lambda k=event.keysym: self._faces_frame.set_annotation_display(k),
"f10": lambda k=event.keysym: self._faces_frame.set_annotation_display(k),
"c": lambda f=tk_pos.get(), d="prev": self._detected_faces.update.copy(f, d),
"v": lambda f=tk_pos.get(), d="next": self._detected_faces.update.copy(f, d),
"c": lambda f=globs.frame_index, d="prev": self._detected_faces.update.copy(f, d),
"v": lambda f=globs.frame_index, d="next": self._detected_faces.update.copy(f, d),
"ctrl_s": self._detected_faces.save,
"r": lambda f=tk_pos.get(): self._detected_faces.revert_to_saved(f)}
"r": lambda f=globs.frame_index: self._detected_faces.revert_to_saved(f)}
# Allow keypad keys to be used for numbers
press = event.keysym.replace("KP_", "") if event.keysym.startswith("KP_") else event.keysym
assert isinstance(event.state, int)
modifier = "_".join(val for key, val in modifiers.items() if event.state & key != 0)
key_press = "_".join([modifier, press]) if modifier else press
if key_press.lower() in bindings:
logger.trace("key press: %s, action: %s", key_press, bindings[key_press.lower()])
logger.trace("key press: %s, action: %s", # type:ignore[attr-defined]
key_press, bindings[key_press.lower()])
self.focus_set()
bindings[key_press.lower()]()
def _set_initial_layout(self):
def _set_initial_layout(self) -> None:
""" Set the favicon and the bottom frame position to correct location to display full
frame window.
@ -271,12 +293,13 @@ class Manual(tk.Tk):
logger.debug("Setting initial layout")
self.tk.call("wm",
"iconphoto",
self._w, get_images().icons["favicon"]) # pylint:disable=protected-access
self._w, # type:ignore[attr-defined] # pylint:disable=protected-access
get_images().icons["favicon"])
location = int(self.winfo_screenheight() // 1.5)
self._containers["main"].sashpos(0, location)
self._containers.main.sashpos(0, location)
self.update_idletasks()
def process(self):
def process(self) -> None:
""" The entry point for the Visual Alignments tool from :mod:`lib.tools.manual.cli`.
Launch the tkinter Visual Alignments Window and run main loop.
@ -289,6 +312,8 @@ class _Options(ttk.Frame): # pylint:disable=too-many-ancestors
""" Control panel options for currently displayed Editor. This is the right hand panel of the
GUI that holds editor specific settings and annotation display settings.
Parameters
----------
parent: :class:`tkinter.ttk.Frame`
The parent frame for the control panel options
tk_globals: :class:`~tools.manual.manual.TkGlobals`
@ -296,9 +321,11 @@ class _Options(ttk.Frame): # pylint:disable=too-many-ancestors
display_frame: :class:`DisplayFrame`
The frame that holds the editors
"""
def __init__(self, parent, tk_globals, display_frame):
logger.debug("Initializing %s: (parent: %s, tk_globals: %s, display_frame: %s)",
self.__class__.__name__, parent, tk_globals, display_frame)
def __init__(self,
parent: ttk.Frame,
tk_globals: TkGlobals,
display_frame: DisplayFrame) -> None:
logger.debug(parse_class_init(locals()))
super().__init__(parent)
self._globals = tk_globals
@ -309,7 +336,7 @@ class _Options(ttk.Frame): # pylint:disable=too-many-ancestors
self.pack(side=tk.RIGHT, fill=tk.Y)
logger.debug("Initialized %s", self.__class__.__name__)
def _initialize(self):
def _initialize(self) -> dict[str, ControlPanel]:
""" Initialize all of the control panels, then display the default panel.
Adds the control panel to :attr:`_control_panels` and sets the traceback to update
@ -322,6 +349,11 @@ class _Options(ttk.Frame): # pylint:disable=too-many-ancestors
The Traceback must be set after the panel has first been packed as otherwise it interferes
with the loading of the faces pane.
Returns
-------
dict[str, :class:`~lib.gui.control_helper.ControlPanel`]
The configured control panels
"""
self._initialize_face_options()
frame = ttk.Frame(self)
@ -343,7 +375,7 @@ class _Options(ttk.Frame): # pylint:disable=too-many-ancestors
panels[name] = panel
return panels
def _initialize_face_options(self):
def _initialize_face_options(self) -> None:
""" Set the Face Viewer options panel, beneath the standard control options. """
frame = ttk.Frame(self)
frame.pack(side=tk.BOTTOM, fill=tk.X, padx=5, pady=5)
@ -352,13 +384,13 @@ class _Options(ttk.Frame): # pylint:disable=too-many-ancestors
lbl = ttk.Label(size_frame, text="Face Size:")
lbl.pack(side=tk.LEFT)
cmb = ttk.Combobox(size_frame,
value=["Tiny", "Small", "Medium", "Large", "Extra Large"],
values=["Tiny", "Small", "Medium", "Large", "Extra Large"],
state="readonly",
textvariable=self._globals.tk_faces_size)
self._globals.tk_faces_size.set("Medium")
textvariable=self._globals.var_faces_size)
self._globals.var_faces_size.set("Medium")
cmb.pack(side=tk.RIGHT, padx=5)
def _set_tk_callbacks(self):
def _set_tk_callbacks(self) -> None:
""" Sets the callback to change to the relevant control panel options when the selected
editor is changed, and the display update on panel option change."""
self._display_frame.tk_selected_action.trace("w", self._update_options)
@ -372,9 +404,9 @@ class _Options(ttk.Frame): # pylint:disable=too-many-ancestors
logger.debug("Adding control update callback: (editor: %s, control: %s)",
name, ctl.title)
seen_controls.add(ctl)
ctl.tk_var.trace("w", lambda *e: self._globals.tk_update.set(True))
ctl.tk_var.trace("w", lambda *e: self._globals.var_full_update.set(True))
def _update_options(self, *args): # pylint:disable=unused-argument
def _update_options(self, *args) -> None: # pylint:disable=unused-argument
""" Update the control panel display for the current editor.
If the options have not already been set, then adds the control panel to
@ -390,7 +422,7 @@ class _Options(ttk.Frame): # pylint:disable=too-many-ancestors
logger.debug("Displaying control panel for editor: '%s'", editor)
self._control_panels[editor].pack(expand=True, fill=tk.BOTH)
def _clear_options_frame(self):
def _clear_options_frame(self) -> None:
""" Hides the currently displayed control panel """
for editor, panel in self._control_panels.items():
if panel.winfo_ismapped():
@ -398,244 +430,6 @@ class _Options(ttk.Frame): # pylint:disable=too-many-ancestors
panel.pack_forget()
class TkGlobals():
""" Holds Tkinter Variables and other frame information that need to be accessible from all
areas of the GUI.
Parameters
----------
input_location: str
The location of the input folder of frames or video file
"""
def __init__(self, input_location):
logger.debug("Initializing %s: (input_location: %s)",
self.__class__.__name__, input_location)
self._tk_vars = self._get_tk_vars()
self._is_video = self._check_input(input_location)
self._frame_count = 0 # set by FrameLoader
self._frame_display_dims = (int(round(896 * get_config().scaling_factor)),
int(round(504 * get_config().scaling_factor)))
self._current_frame = {"image": None,
"scale": None,
"interpolation": None,
"display_dims": None,
"filename": None}
logger.debug("Initialized %s", self.__class__.__name__)
@classmethod
def _get_tk_vars(cls):
""" Create and initialize the tkinter variables.
Returns
-------
dict
The variable name as key, the variable as value
"""
retval = {}
for name in ("frame_index", "transport_index", "face_index", "filter_distance"):
var = tk.IntVar()
var.set(10 if name == "filter_distance" else 0)
retval[name] = var
for name in ("update", "update_active_viewport", "is_zoomed"):
var = tk.BooleanVar()
var.set(False)
retval[name] = var
for name in ("filter_mode", "faces_size"):
retval[name] = tk.StringVar()
return retval
@property
def current_frame(self):
""" dict: The currently displayed frame in the frame viewer with it's meta information. Key
and Values are as follows:
**image** (:class:`numpy.ndarry`): The currently displayed frame in original dimensions
**scale** (`float`): The scaling factor to use to resize the image to the display
window
**interpolation** (`int`): The opencv interpolator ID to use for resizing the image to
the display window
**display_dims** (`tuple`): The size of the currently displayed frame, sized for the
display window
**filename** (`str`): The filename of the currently displayed frame
"""
return self._current_frame
@property
def frame_count(self):
""" int: The total number of frames for the input location """
return self._frame_count
@property
def tk_face_index(self):
""" :class:`tkinter.IntVar`: The variable that holds the face index of the selected face
within the current frame when in zoomed mode. """
return self._tk_vars["face_index"]
@property
def tk_update_active_viewport(self):
""" :class:`tkinter.BooleanVar`: Boolean Variable that is traced by the viewport's active
frame to update.. """
return self._tk_vars["update_active_viewport"]
@property
def face_index(self):
""" int: The currently displayed face index when in zoomed mode. """
return self._tk_vars["face_index"].get()
@property
def frame_display_dims(self):
""" tuple: The (`width`, `height`) of the video display frame in pixels. """
return self._frame_display_dims
@property
def frame_index(self):
""" int: The currently displayed frame index. NB This returns -1 if there are no frames
that meet the currently selected filter criteria. """
return self._tk_vars["frame_index"].get()
@property
def tk_frame_index(self):
""" :class:`tkinter.IntVar`: The variable holding the current frame index. """
return self._tk_vars["frame_index"]
@property
def filter_mode(self):
""" str: The currently selected navigation mode. """
return self._tk_vars["filter_mode"].get()
@property
def tk_filter_mode(self):
""" :class:`tkinter.StringVar`: The variable holding the currently selected navigation
filter mode. """
return self._tk_vars["filter_mode"]
@property
def tk_filter_distance(self):
""" :class:`tkinter.DoubleVar`: The variable holding the currently selected threshold
distance for misaligned filter mode. """
return self._tk_vars["filter_distance"]
@property
def tk_faces_size(self):
""" :class:`tkinter.StringVar`: The variable holding the currently selected Faces Viewer
thumbnail size. """
return self._tk_vars["faces_size"]
@property
def is_video(self):
""" bool: ``True`` if the input is a video file, ``False`` if it is a folder of images. """
return self._is_video
@property
def tk_is_zoomed(self):
""" :class:`tkinter.BooleanVar`: The variable holding the value indicating whether the
frame viewer is zoomed into a face or zoomed out to the full frame. """
return self._tk_vars["is_zoomed"]
@property
def is_zoomed(self):
""" bool: ``True`` if the frame viewer is zoomed into a face, ``False`` if the frame viewer
is displaying a full frame. """
return self._tk_vars["is_zoomed"].get()
@property
def tk_transport_index(self):
""" :class:`tkinter.IntVar`: The current index of the display frame's transport slider. """
return self._tk_vars["transport_index"]
@property
def tk_update(self):
""" :class:`tkinter.BooleanVar`: The variable holding the trigger that indicates that a
full update needs to occur. """
return self._tk_vars["update"]
@staticmethod
def _check_input(frames_location):
""" Check whether the input is a video
Parameters
----------
frames_location: str
The input location for video or images
Returns
-------
bool: 'True' if input is a video 'False' if it is a folder.
"""
if os.path.isdir(frames_location):
retval = False
elif os.path.splitext(frames_location)[1].lower() in VIDEO_EXTENSIONS:
retval = True
else:
logger.error("The input location '%s' is not valid", frames_location)
sys.exit(1)
logger.debug("Input '%s' is_video: %s", frames_location, retval)
return retval
def set_frame_count(self, count):
""" Set the count of total number of frames to :attr:`frame_count` when the
:class:`FramesLoader` has completed loading.
Parameters
----------
count: int
The number of frames that exist for this session
"""
logger.debug("Setting frame_count to : %s", count)
self._frame_count = count
def set_current_frame(self, image, filename):
""" Set the frame and meta information for the currently displayed frame. Populates the
attribute :attr:`current_frame`
Parameters
----------
image: :class:`numpy.ndarray`
The image used to display in the Frame Viewer
filename: str
The filename of the current frame
"""
scale = min(self.frame_display_dims[0] / image.shape[1],
self.frame_display_dims[1] / image.shape[0])
self._current_frame["image"] = image
self._current_frame["filename"] = filename
self._current_frame["scale"] = scale
self._current_frame["interpolation"] = cv2.INTER_CUBIC if scale > 1.0 else cv2.INTER_AREA
self._current_frame["display_dims"] = (int(round(image.shape[1] * scale)),
int(round(image.shape[0] * scale)))
logger.trace({k: v.shape if isinstance(v, np.ndarray) else v
for k, v in self._current_frame.items()})
def set_frame_display_dims(self, width, height):
""" Set the size, in pixels, of the video frame display window and resize the displayed
frame.
Used on a frame resize callback, sets the :attr:frame_display_dims`.
Parameters
----------
width: int
The width of the frame holding the video canvas in pixels
height: int
The height of the frame holding the video canvas in pixels
"""
self._frame_display_dims = (int(width), int(height))
image = self._current_frame["image"]
scale = min(self.frame_display_dims[0] / image.shape[1],
self.frame_display_dims[1] / image.shape[0])
self._current_frame["scale"] = scale
self._current_frame["interpolation"] = cv2.INTER_CUBIC if scale > 1.0 else cv2.INTER_AREA
self._current_frame["display_dims"] = (int(round(image.shape[1] * scale)),
int(round(image.shape[0] * scale)))
logger.trace({k: v.shape if isinstance(v, np.ndarray) else v
for k, v in self._current_frame.items()})
class Aligner():
""" The :class:`Aligner` class sets up an extraction pipeline for each of the current Faceswap
Aligners, along with the Landmarks based Maskers. When new landmarks are required, the bounding
@ -684,8 +478,8 @@ class Aligner():
assert self._detected_faces is not None
face = self._detected_faces.current_faces[self._frame_index][self._face_index]
return ExtractMedia(
self._globals.current_frame["filename"],
self._globals.current_frame["image"],
self._globals.current_frame.filename,
self._globals.current_frame.image,
detected_faces=[face])
@property
@ -864,53 +658,93 @@ class FrameLoader():
The path to the input frames
video_meta_data: dict
The meta data held within the alignments file, if it exists and the input is a video
file_list: list[str]
The list of filenames that exist within the alignments file
"""
def __init__(self, tk_globals, frames_location, video_meta_data):
logger.debug("Initializing %s: (tk_globals: %s, frames_location: '%s', "
"video_meta_data: %s)", self.__class__.__name__, tk_globals, frames_location,
video_meta_data)
def __init__(self,
tk_globals: TkGlobals,
frames_location: str,
video_meta_data: dict[str, list[int] | list[float] | None],
file_list: list[str]) -> None:
logger.debug(parse_class_init(locals()))
self._globals = tk_globals
self._loader = None
self._loader: SingleFrameLoader | None = None
self._current_idx = 0
self._init_thread = self._background_init_frames(frames_location, video_meta_data)
self._globals.tk_frame_index.trace("w", self._set_frame)
self._init_thread = self._background_init_frames(frames_location,
video_meta_data,
file_list)
self._globals.var_frame_index.trace_add("write", self._set_frame)
logger.debug("Initialized %s", self.__class__.__name__)
@property
def is_initialized(self):
""" bool: ``True`` if the Frame Loader has completed initialization otherwise
``False``. """
def is_initialized(self) -> bool:
""" bool: ``True`` if the Frame Loader has completed initialization. """
thread_is_alive = self._init_thread.is_alive()
if thread_is_alive:
self._init_thread.check_and_raise_error()
else:
self._init_thread.join()
# Setting the initial frame cannot be done in the thread, so set when queried from main
self._set_frame(initialize=True)
self._set_frame(initialize=True) # Setting initial frame must be done from main thread
return not thread_is_alive
@property
def video_meta_data(self):
def video_meta_data(self) -> dict[str, list[int] | list[float] | None]:
""" dict: The pts_time and key frames for the loader. """
assert self._loader is not None
return self._loader.video_meta_data
def _background_init_frames(self, frames_location, video_meta_data):
def _background_init_frames(self,
frames_location: str,
video_meta_data: dict[str, list[int] | list[float] | None],
frame_list: list[str]) -> MultiThread:
""" Launch the images loader in a background thread so we can run other tasks whilst
waiting for initialization. """
waiting for initialization.
Parameters
----------
frame_location: str
The location of the source video file/frames folder
video_meta_data: dict
The meta data for video file sources
frame_list: list[str]
The list of frames that exist in the alignments file
"""
thread = MultiThread(self._load_images,
frames_location,
video_meta_data,
frame_list,
thread_count=1,
name=f"{self.__class__.__name__}.init_frames")
thread.start()
return thread
def _load_images(self, frames_location, video_meta_data):
""" Load the images in a background thread. """
self._loader = SingleFrameLoader(frames_location, video_meta_data=video_meta_data)
self._globals.set_frame_count(self._loader.count)
def _load_images(self,
frames_location: str,
video_meta_data: dict[str, list[int] | list[float] | None],
frame_list: list[str]) -> None:
""" Load the images in a background thread.
def _set_frame(self, *args, initialize=False): # pylint:disable=unused-argument
Parameters
----------
frame_location: str
The location of the source video file/frames folder
video_meta_data: dict
The meta data for video file sources
frame_list: list[str]
The list of frames that exist in the alignments file
"""
self._loader = SingleFrameLoader(frames_location, video_meta_data=video_meta_data)
if not self._loader.is_video and len(frame_list) < self._loader.count:
files = [os.path.basename(f) for f in self._loader.file_list]
skip_list = [idx for idx, fname in enumerate(files) if fname not in frame_list]
logger.debug("Adding %s entries to skip list for images not in alignments file",
len(skip_list))
self._loader.add_skip_list(skip_list)
self._globals.set_frame_count(self._loader.process_count)
def _set_frame(self, # pylint:disable=unused-argument
*args,
initialize: bool = False) -> None:
""" Set the currently loaded frame to :attr:`_current_frame` and trigger a full GUI update.
If the loader has not been initialized, or the navigation position is the same as the
@ -926,17 +760,19 @@ class FrameLoader():
"""
position = self._globals.frame_index
if not initialize and (position == self._current_idx and not self._globals.is_zoomed):
logger.trace("Update criteria not met. Not updating: (initialize: %s, position: %s, "
"current_idx: %s, is_zoomed: %s)", initialize, position,
self._current_idx, self._globals.is_zoomed)
logger.trace("Update criteria not met. Not updating: " # type:ignore[attr-defined]
"(initialize: %s, position: %s, current_idx: %s, is_zoomed: %s)",
initialize, position, self._current_idx, self._globals.is_zoomed)
return
if position == -1:
filename = "No Frame"
frame = np.ones(self._globals.frame_display_dims + (3, ), dtype="uint8")
else:
assert self._loader is not None
filename, frame = self._loader.image_from_index(position)
logger.trace("filename: %s, frame: %s, position: %s", filename, frame.shape, position)
logger.trace("filename: %s, frame: %s, position: %s", # type:ignore[attr-defined]
filename, frame.shape, position)
self._globals.set_current_frame(frame, filename)
self._current_idx = position
self._globals.tk_update.set(True)
self._globals.tk_update_active_viewport.set(True)
self._globals.var_full_update.set(True)
self._globals.var_update_active_viewport.set(True)