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

301 lines
12 KiB
Python

#!/usr/bin/env python3
""" Handles Navigation and Background Image for the Frame Viewer section of the manual
tool GUI. """
import logging
import tkinter as tk
import cv2
import numpy as np
from PIL import Image, ImageTk
from lib.align import AlignedFace
logger = logging.getLogger(__name__)
class Navigation():
""" Handles playback and frame navigation for the Frame Viewer Window.
Parameters
----------
display_frame: :class:`DisplayFrame`
The parent frame viewer window
"""
def __init__(self, display_frame):
logger.debug("Initializing %s", self.__class__.__name__)
self._display_frame = display_frame
self._globals = display_frame._globals
self._det_faces = display_frame._det_faces
self._nav = display_frame._nav
self._tk_is_playing = tk.BooleanVar()
self._tk_is_playing.set(False)
self._det_faces.tk_face_count_changed.trace("w", self._update_total_frame_count)
logger.debug("Initialized %s", self.__class__.__name__)
@property
def _current_nav_frame_count(self):
""" int: The current frame count for the transport slider """
return self._nav["scale"].cget("to") + 1
def nav_scale_callback(self, *args, reset_progress=True): # pylint:disable=unused-argument
""" Adjust transport slider scale for different filters. Hide or display optional filter
controls.
"""
self._display_frame.pack_threshold_slider()
if reset_progress:
self.stop_playback()
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.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.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.
Parameters
----------
args: tuple
Required for tkinter trace callback but unused
"""
frame_count = self._det_faces.filter.count
if self._current_nav_frame_count == frame_count:
logger.trace("Filtered count has not changed. Returning")
return
max_frame = max(0, frame_count - 1)
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=f"/{max_frame}")
state = "disabled" if max_frame == 0 else "normal"
self._nav["entry"].config(state=state)
@property
def tk_is_playing(self):
""" :class:`tkinter.BooleanVar`: Whether the stream is currently playing. """
return self._tk_is_playing
def handle_play_button(self):
""" Handle the play button.
Switches the :attr:`tk_is_playing` variable.
"""
is_playing = self.tk_is_playing.get()
self.tk_is_playing.set(not is_playing)
def stop_playback(self):
""" Stop play back if playing """
if self.tk_is_playing.get():
logger.trace("Stopping playback")
self.tk_is_playing.set(False)
def increment_frame(self, frame_count=None, is_playing=False):
""" Update The frame navigation position to the next frame based on filter. """
if not is_playing:
self.stop_playback()
position = self._get_safe_frame_index()
face_count_change = not self._det_faces.filter.frame_meets_criteria
if face_count_change:
position -= 1
frame_count = self._det_faces.filter.count if frame_count is None else frame_count
if not face_count_change and (frame_count == 0 or position == frame_count - 1):
logger.debug("End of Stream. Not incrementing")
self.stop_playback()
return
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. """
self.stop_playback()
position = self._get_safe_frame_index()
face_count_change = not self._det_faces.filter.frame_meets_criteria
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.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 var_transport_index variable in
a safe manner (i.e. handle for non-numeric)
Returns
-------
int
The current transport frame index
"""
try:
retval = self._globals.var_transport_index.get()
except tk.TclError as err:
if "expected floating-point" not in str(err):
raise
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.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.var_transport_index.get()
if position == 0:
return
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.var_transport_index.get()
frame_count = self._det_faces.filter.count
if position == frame_count - 1:
return
self._globals.var_transport_index.set(frame_count - 1)
class BackgroundImage():
""" The background image of the canvas """
def __init__(self, canvas):
self._canvas = canvas
self._globals = canvas._globals
self._det_faces = canvas._det_faces
placeholder = np.ones((*reversed(self._globals.frame_display_dims), 3), dtype="uint8")
self._tk_frame = ImageTk.PhotoImage(Image.fromarray(placeholder))
self._tk_face = ImageTk.PhotoImage(Image.fromarray(placeholder))
self._image = self._canvas.create_image(self._globals.frame_display_dims[0] / 2,
self._globals.frame_display_dims[1] / 2,
image=self._tk_frame,
anchor=tk.CENTER,
tags="main_image")
self._zoomed_centering = "face"
@property
def _current_view_mode(self):
""" str: `frame` if global zoom mode variable is set to ``False`` other wise `face`. """
retval = "face" if self._globals.is_zoomed else "frame"
logger.trace(retval)
return retval
def refresh(self, view_mode):
""" Update the displayed frame.
Parameters
----------
view_mode: ["frame", "face"]
The currently active editor's selected view mode.
"""
self._switch_image(view_mode)
logger.trace("Updating background frame")
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.
Parameters
----------
view_mode: ["frame", "face"]
The currently active editor's selected view mode.
"""
if view_mode == self._current_view_mode and (
self._canvas.active_editor.zoomed_centering == self._zoomed_centering):
return
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, f"_tk_{view_mode}")
self._canvas.itemconfig(self._image, image=img)
self._globals.set_zoomed(view_mode == "face")
self._globals.set_face_index(0)
def _update_tk_face(self):
""" Update the currently zoomed face. """
face = self._get_zoomed_face()
padding = self._get_padding((min(self._globals.frame_display_dims),
min(self._globals.frame_display_dims)))
face = cv2.copyMakeBorder(face, *padding, cv2.BORDER_CONSTANT)
if self._tk_frame.height() != face.shape[0]:
self._resize_frame()
logger.trace("final shape: %s", face.shape)
self._tk_face.paste(Image.fromarray(face))
def _get_zoomed_face(self):
""" Get the zoomed face or a blank image if no faces are available.
Returns
-------
:class:`numpy.ndarray`
The face sized to the shortest dimensions of the face viewer
"""
frame_idx = self._globals.frame_index
face_idx = self._globals.face_index
faces_in_frame = self._det_faces.face_count_per_index[frame_idx]
size = min(self._globals.frame_display_dims)
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.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,
centering=self._zoomed_centering,
size=size).face
logger.trace("face shape: %s", face.shape)
return face[..., 2::-1]
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]
padding = self._get_padding(img.shape[:2])
if any(padding):
img = cv2.copyMakeBorder(img, *padding, cv2.BORDER_CONSTANT)
logger.trace("final shape: %s", img.shape)
if self._tk_frame.height() != img.shape[0]:
self._resize_frame()
self._tk_frame.paste(Image.fromarray(img))
def _get_padding(self, size):
""" Obtain the Left, Top, Right, Bottom padding required to place the square face or frame
in to the Photo Image
Returns
-------
tuple
The (Left, Top, Right, Bottom) padding to apply to the face image in pixels
"""
pad_lt = ((self._globals.frame_display_dims[1] - size[0]) // 2,
(self._globals.frame_display_dims[0] - size[1]) // 2)
padding = (pad_lt[0],
self._globals.frame_display_dims[1] - size[0] - pad_lt[0],
pad_lt[1],
self._globals.frame_display_dims[0] - size[1] - pad_lt[1])
logger.debug("Frame dimensions: %s, size: %s, padding: %s",
self._globals.frame_display_dims, size, padding)
return padding
def _resize_frame(self):
""" Resize the :attr:`_tk_frame`, attr:`_tk_face` photo images, update the canvas to
offset the image correctly.
"""
logger.trace("Resizing video frame on resize event: %s", self._globals.frame_display_dims)
placeholder = np.ones((*reversed(self._globals.frame_display_dims), 3), dtype="uint8")
self._tk_frame = ImageTk.PhotoImage(Image.fromarray(placeholder))
self._tk_face = ImageTk.PhotoImage(Image.fromarray(placeholder))
self._canvas.coords(self._image,
self._globals.frame_display_dims[0] / 2,
self._globals.frame_display_dims[1] / 2)
img = self._tk_face if self._current_view_mode == "face" else self._tk_frame
self._canvas.itemconfig(self._image, image=img)