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:
parent
ea63f1e64a
commit
b6ac7b8039
14 changed files with 657 additions and 477 deletions
|
@ -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
|
||||
==================
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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. """
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
309
tools/manual/globals.py
Normal 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)
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Reference in a new issue