mirror of
https://github.com/deepfakes/faceswap
synced 2025-06-07 10:43:27 -04:00
* model_refactor (#571) * original model to new structure * IAE model to new structure * OriginalHiRes to new structure * Fix trainer for different resolutions * Initial config implementation * Configparse library added * improved training data loader * dfaker model working * Add logging to training functions * Non blocking input for cli training * Add error handling to threads. Add non-mp queues to queue_handler * Improved Model Building and NNMeta * refactor lib/models * training refactor. DFL H128 model Implementation * Dfaker - use hashes * Move timelapse. Remove perceptual loss arg * Update INSTALL.md. Add logger formatting. Update Dfaker training * DFL h128 partially ported * Add mask to dfaker (#573) * Remove old models. Add mask to dfaker * dfl mask. Make masks selectable in config (#575) * DFL H128 Mask. Mask type selectable in config. * remove gan_v2_2 * Creating Input Size config for models Creating Input Size config for models Will be used downstream in converters. Also name change of image_shape to input_shape to clarify ( for future models with potentially different output_shapes) * Add mask loss options to config * MTCNN options to config.ini. Remove GAN config. Update USAGE.md * Add sliders for numerical values in GUI * Add config plugins menu to gui. Validate config * Only backup model if loss has dropped. Get training working again * bugfixes * Standardise loss printing * GUI idle cpu fixes. Graph loss fix. * mutli-gpu logging bugfix * Merge branch 'staging' into train_refactor * backup state file * Crash protection: Only backup if both total losses have dropped * Port OriginalHiRes_RC4 to train_refactor (OriginalHiRes) * Load and save model structure with weights * Slight code update * Improve config loader. Add subpixel opt to all models. Config to state * Show samples... wrong input * Remove AE topology. Add input/output shapes to State * Port original_villain (birb/VillainGuy) model to faceswap * Add plugin info to GUI config pages * Load input shape from state. IAE Config options. * Fix transform_kwargs. Coverage to ratio. Bugfix mask detection * Suppress keras userwarnings. Automate zoom. Coverage_ratio to model def. * Consolidation of converters & refactor (#574) * Consolidation of converters & refactor Initial Upload of alpha Items - consolidate convert_mased & convert_adjust into one converter -add average color adjust to convert_masked -allow mask transition blur size to be a fixed integer of pixels and a fraction of the facial mask size -allow erosion/dilation size to be a fixed integer of pixels and a fraction of the facial mask size -eliminate redundant type conversions to avoid multiple round-off errors -refactor loops for vectorization/speed -reorganize for clarity & style changes TODO - bug/issues with warping the new face onto a transparent old image...use a cleanup mask for now - issues with mask border giving black ring at zero erosion .. investigate - remove GAN ?? - test enlargment factors of umeyama standard face .. match to coverage factor - make enlargment factor a model parameter - remove convert_adjusted and referencing code when finished * Update Convert_Masked.py default blur size of 2 to match original... description of enlargement tests breakout matrxi scaling into def * Enlargment scale as a cli parameter * Update cli.py * dynamic interpolation algorithm Compute x & y scale factors from the affine matrix on the fly by QR decomp. Choose interpolation alogrithm for the affine warp based on an upsample or downsample for each image * input size input size from config * fix issues with <1.0 erosion * Update convert.py * Update Convert_Adjust.py more work on the way to merginf * Clean up help note on sharpen * cleanup seamless * Delete Convert_Adjust.py * Update umeyama.py * Update training_data.py * swapping * segmentation stub * changes to convert.str * Update masked.py * Backwards compatibility fix for models Get converter running * Convert: Move masks to class. bugfix blur_size some linting * mask fix * convert fixes - missing facehull_rect re-added - coverage to % - corrected coverage logic - cleanup of gui option ordering * Update cli.py * default for blur * Update masked.py * added preliminary low_mem version of OriginalHighRes model plugin * Code cleanup, minor fixes * Update masked.py * Update masked.py * Add dfl mask to convert * histogram fix & seamless location * update * revert * bugfix: Load actual configuration in gui * Standardize nn_blocks * Update cli.py * Minor code amends * Fix Original HiRes model * Add masks to preview output for mask trainers refactor trainer.__base.py * Masked trainers converter support * convert bugfix * Bugfix: Converter for masked (dfl/dfaker) trainers * Additional Losses (#592) * initial upload * Delete blur.py * default initializer = He instead of Glorot (#588) * Allow kernel_initializer to be overridable * Add ICNR Initializer option for upscale on all models. * Hopefully fixes RSoDs with original-highres model plugin * remove debug line * Original-HighRes model plugin Red Screen of Death fix, take #2 * Move global options to _base. Rename Villain model * clipnorm and res block biases * scale the end of res block * res block * dfaker pre-activation res * OHRES pre-activation * villain pre-activation * tabs/space in nn_blocks * fix for histogram with mask all set to zero * fix to prevent two networks with same name * GUI: Wider tooltips. Improve TQDM capture * Fix regex bug * Convert padding=48 to ratio of image size * Add size option to alignments tool extract * Pass through training image size to convert from model * Convert: Pull training coverage from model * convert: coverage, blur and erode to percent * simplify matrix scaling * ordering of sliders in train * Add matrix scaling to utils. Use interpolation in lib.aligner transform * masked.py Import get_matrix_scaling from utils * fix circular import * Update masked.py * quick fix for matrix scaling * testing thus for now * tqdm regex capture bugfix * Minor ammends * blur size cleanup * Remove coverage option from convert (Now cascades from model) * Implement convert for all model types * Add mask option and coverage option to all existing models * bugfix for model loading on convert * debug print removal * Bugfix for masks in dfl_h128 and iae * Update preview display. Add preview scaling to cli * mask notes * Delete training_data_v2.py errant file * training data variables * Fix timelapse function * Add new config items to state file for legacy purposes * Slight GUI tweak * Raise exception if problem with loaded model * Add Tensorboard support (Logs stored in model directory) * ICNR fix * loss bugfix * convert bugfix * Move ini files to config folder. Make TensorBoard optional * Fix training data for unbalanced inputs/outputs * Fix config "none" test * Keep helptext in .ini files when saving config from GUI * Remove frame_dims from alignments * Add no-flip and warp-to-landmarks cli options * Revert OHR to RC4_fix version * Fix lowmem mode on OHR model * padding to variable * Save models in parallel threads * Speed-up of res_block stability * Automated Reflection Padding * Reflect Padding as a training option Includes auto-calculation of proper padding shapes, input_shapes, output_shapes Flag included in config now * rest of reflect padding * Move TB logging to cli. Session info to state file * Add session iterations to state file * Add recent files to menu. GUI code tidy up * [GUI] Fix recent file list update issue * Add correct loss names to TensorBoard logs * Update live graph to use TensorBoard and remove animation * Fix analysis tab. GUI optimizations * Analysis Graph popup to Tensorboard Logs * [GUI] Bug fix for graphing for models with hypens in name * [GUI] Correctly split loss to tabs during training * [GUI] Add loss type selection to analysis graph * Fix store command name in recent files. Switch to correct tab on open * [GUI] Disable training graph when 'no-logs' is selected * Fix graphing race condition * rename original_hires model to unbalanced
990 lines
40 KiB
Python
990 lines
40 KiB
Python
#!/usr/bin/env python3
|
|
""" Manual processing of alignments """
|
|
|
|
import logging
|
|
import platform
|
|
import sys
|
|
import cv2
|
|
import numpy as np
|
|
|
|
from lib.multithreading import SpawnProcess
|
|
from lib.queue_manager import queue_manager, QueueEmpty
|
|
from plugins.plugin_loader import PluginLoader
|
|
from . import Annotate, ExtractedFaces, Frames, Legacy
|
|
|
|
logger = logging.getLogger(__name__) # pylint: disable=invalid-name
|
|
|
|
|
|
class Interface():
|
|
""" Key controls and interfacing options for OpenCV """
|
|
def __init__(self, alignments, frames):
|
|
logger.debug("Initializing %s: (alignments: %s, frames: %s)",
|
|
self.__class__.__name__, alignments, frames)
|
|
self.alignments = alignments
|
|
self.frames = frames
|
|
self.controls = self.set_controls()
|
|
self.state = self.set_state()
|
|
self.skip_mode = {1: "Standard",
|
|
2: "No Faces",
|
|
3: "Multi-Faces",
|
|
4: "Has Faces"}
|
|
logger.debug("Initialized %s", self.__class__.__name__)
|
|
|
|
def set_controls(self):
|
|
""" Set keyboard controls, destination and help text """
|
|
controls = {"z": {"action": self.iterate_frame,
|
|
"args": ("navigation", - 1),
|
|
"help": "Previous Frame"},
|
|
"x": {"action": self.iterate_frame,
|
|
"args": ("navigation", 1),
|
|
"help": "Next Frame"},
|
|
"[": {"action": self.iterate_frame,
|
|
"args": ("navigation", - 100),
|
|
"help": "100 Frames Back"},
|
|
"]": {"action": self.iterate_frame,
|
|
"args": ("navigation", 100),
|
|
"help": "100 Frames Forward"},
|
|
"{": {"action": self.iterate_frame,
|
|
"args": ("navigation", "first"),
|
|
"help": "Go to First Frame"},
|
|
"}": {"action": self.iterate_frame,
|
|
"args": ("navigation", "last"),
|
|
"help": "Go to Last Frame"},
|
|
27: {"action": "quit",
|
|
"key_text": "ESC",
|
|
"args": ("navigation", None),
|
|
"help": "Exit",
|
|
"key_type": ord},
|
|
"/": {"action": self.iterate_state,
|
|
"args": ("navigation", "frame-size"),
|
|
"help": "Cycle Frame Zoom"},
|
|
"s": {"action": self.iterate_state,
|
|
"args": ("navigation", "skip-mode"),
|
|
"help": ("Skip Mode (All, No Faces, Multi Faces, Has Faces)")},
|
|
" ": {"action": self.save_alignments,
|
|
"key_text": "SPACE",
|
|
"args": ("edit", None),
|
|
"help": "Save Alignments"},
|
|
"r": {"action": self.reload_alignments,
|
|
"args": ("edit", None),
|
|
"help": "Reload Alignments (Discard all changes)"},
|
|
"d": {"action": self.delete_alignment,
|
|
"args": ("edit", None),
|
|
"help": "Delete Selected Alignment"},
|
|
"m": {"action": self.toggle_state,
|
|
"args": ("edit", "active"),
|
|
"help": "Change Mode (View, Edit)"},
|
|
range(10): {"action": self.set_state_value,
|
|
"key_text": "0 to 9",
|
|
"args": ["edit", "selected"],
|
|
"help": "Select/Deselect Face at this Index",
|
|
"key_type": range},
|
|
"c": {"action": self.copy_alignments,
|
|
"args": ("edit", -1),
|
|
"help": "Copy Previous Frame's Alignments"},
|
|
"v": {"action": self.copy_alignments,
|
|
"args": ("edit", 1),
|
|
"help": "Copy Next Frame's Alignments"},
|
|
"y": {"action": self.toggle_state,
|
|
"args": ("image", "display"),
|
|
"help": "Toggle Image"},
|
|
"u": {"action": self.iterate_state,
|
|
"args": ("bounding_box", "color"),
|
|
"help": "Cycle Bounding Box Color"},
|
|
"i": {"action": self.iterate_state,
|
|
"args": ("extract_box", "color"),
|
|
"help": "Cycle Extract Box Color"},
|
|
"o": {"action": self.iterate_state,
|
|
"args": ("landmarks", "color"),
|
|
"help": "Cycle Landmarks Color"},
|
|
"p": {"action": self.iterate_state,
|
|
"args": ("landmarks_mesh", "color"),
|
|
"help": "Cycle Landmarks Mesh Color"},
|
|
"h": {"action": self.iterate_state,
|
|
"args": ("bounding_box", "size"),
|
|
"help": "Cycle Bounding Box thickness"},
|
|
"j": {"action": self.iterate_state,
|
|
"args": ("extract_box", "size"),
|
|
"help": "Cycle Extract Box thickness"},
|
|
"k": {"action": self.iterate_state,
|
|
"args": ("landmarks", "size"),
|
|
"help": "Cycle Landmarks - point size"},
|
|
"l": {"action": self.iterate_state,
|
|
"args": ("landmarks_mesh", "size"),
|
|
"help": "Cycle Landmarks Mesh - thickness"}}
|
|
|
|
logger.debug("Controls: %s", controls)
|
|
return controls
|
|
|
|
@staticmethod
|
|
def set_state():
|
|
""" Set the initial display state """
|
|
state = {"bounding_box": dict(),
|
|
"extract_box": dict(),
|
|
"landmarks": dict(),
|
|
"landmarks_mesh": dict(),
|
|
"image": dict(),
|
|
"navigation": {"skip-mode": 1,
|
|
"frame-size": 1,
|
|
"frame_idx": 0,
|
|
"max_frame": 0,
|
|
"last_request": 0,
|
|
"frame_name": None},
|
|
"edit": {"updated": False,
|
|
"update_faces": False,
|
|
"selected": None,
|
|
"active": 0,
|
|
"redraw": False}}
|
|
|
|
# See lib_alignments/annotate.py for color mapping
|
|
color = 0
|
|
for key in sorted(state.keys()):
|
|
if key not in ("bounding_box", "extract_box", "landmarks", "landmarks_mesh", "image"):
|
|
continue
|
|
state[key]["display"] = True
|
|
if key == "image":
|
|
continue
|
|
color += 1
|
|
state[key]["size"] = 1
|
|
state[key]["color"] = color
|
|
logger.debug("State: %s", state)
|
|
return state
|
|
|
|
def save_alignments(self, *args): # pylint: disable=unused-argument
|
|
""" Save alignments """
|
|
logger.debug("Saving Alignments")
|
|
if not self.state["edit"]["updated"]:
|
|
logger.debug("Save received, but state not updated. Not saving")
|
|
return
|
|
self.alignments.save()
|
|
self.state["edit"]["updated"] = False
|
|
self.set_redraw(True)
|
|
|
|
def reload_alignments(self, *args): # pylint: disable=unused-argument
|
|
""" Reload alignments """
|
|
logger.debug("Reloading Alignments")
|
|
if not self.state["edit"]["updated"]:
|
|
logger.debug("Reload received, but state not updated. Not reloading")
|
|
return
|
|
self.alignments.reload()
|
|
self.state["edit"]["updated"] = False
|
|
self.state["edit"]["update_faces"] = True
|
|
self.set_redraw(True)
|
|
|
|
def delete_alignment(self, *args): # pylint: disable=unused-argument
|
|
""" Save alignments """
|
|
logger.debug("Deleting Alignments")
|
|
selected_face = self.get_selected_face_id()
|
|
if self.get_edit_mode() == "View" or selected_face is None:
|
|
logger.debug("Delete received, but edit mode is 'View'. Not deleting")
|
|
return
|
|
frame = self.get_frame_name()
|
|
if self.alignments.delete_face_at_index(frame, selected_face):
|
|
self.state["edit"]["selected"] = None
|
|
self.state["edit"]["updated"] = True
|
|
self.state["edit"]["update_faces"] = True
|
|
self.set_redraw(True)
|
|
|
|
def copy_alignments(self, *args):
|
|
""" Copy the alignments from the previous or next frame
|
|
to the current frame """
|
|
logger.debug("Copying Alignments")
|
|
if self.get_edit_mode() != "Edit":
|
|
logger.debug("Copy received, but edit mode is not 'Edit'. Not copying")
|
|
return
|
|
frame_id = self.state["navigation"]["frame_idx"] + args[1]
|
|
if not 0 <= frame_id <= self.state["navigation"]["max_frame"]:
|
|
return
|
|
current_frame = self.get_frame_name()
|
|
get_frame = self.frames.file_list_sorted[frame_id]["frame_fullname"]
|
|
alignments = self.alignments.get_faces_in_frame(get_frame)
|
|
for alignment in alignments:
|
|
self.alignments. add_face(current_frame, alignment)
|
|
self.state["edit"]["updated"] = True
|
|
self.state["edit"]["update_faces"] = True
|
|
self.set_redraw(True)
|
|
|
|
def toggle_state(self, item, category):
|
|
""" Toggle state of requested item """
|
|
logger.debug("Toggling state: (item: %s, category: %s)", item, category)
|
|
self.state[item][category] = not self.state[item][category]
|
|
logger.debug("State toggled: (item: %s, category: %s, value: %s)",
|
|
item, category, self.state[item][category])
|
|
self.set_redraw(True)
|
|
|
|
def iterate_state(self, item, category):
|
|
""" Cycle through options (6 possible or 3 currently supported) """
|
|
logger.debug("Cycling state: (item: %s, category: %s)", item, category)
|
|
if category == "color":
|
|
max_val = 7
|
|
elif category == "frame-size":
|
|
max_val = 6
|
|
elif category == "skip-mode":
|
|
max_val = 4
|
|
else:
|
|
max_val = 3
|
|
val = self.state[item][category]
|
|
val = val + 1 if val != max_val else 1
|
|
self.state[item][category] = val
|
|
logger.debug("Cycled state: (item: %s, category: %s, value: %s)",
|
|
item, category, self.state[item][category])
|
|
self.set_redraw(True)
|
|
|
|
def set_state_value(self, item, category, value):
|
|
""" Set state of requested item or toggle off """
|
|
logger.debug("Setting state value: (item: %s, category: %s, value: %s)",
|
|
item, category, value)
|
|
state = self.state[item][category]
|
|
value = str(value) if value is not None else value
|
|
if state == value:
|
|
self.state[item][category] = None
|
|
else:
|
|
self.state[item][category] = value
|
|
logger.debug("Setting state value: (item: %s, category: %s, value: %s)",
|
|
item, category, self.state[item][category])
|
|
self.set_redraw(True)
|
|
|
|
def iterate_frame(self, *args):
|
|
""" Iterate frame up or down, stopping at either end """
|
|
logger.debug("Iterating frame: (args: %s)", args)
|
|
iteration = args[1]
|
|
max_frame = self.state["navigation"]["max_frame"]
|
|
if iteration in ("first", "last"):
|
|
next_frame = 0 if iteration == "first" else max_frame
|
|
self.state["navigation"]["frame_idx"] = next_frame
|
|
self.state["navigation"]["last_request"] = 0
|
|
self.set_redraw(True)
|
|
return
|
|
|
|
current_frame = self.state["navigation"]["frame_idx"]
|
|
next_frame = current_frame + iteration
|
|
end = 0 if iteration < 0 else max_frame
|
|
if (max_frame == 0 or
|
|
(end > 0 and next_frame >= end) or
|
|
(end == 0 and next_frame <= end)):
|
|
next_frame = end
|
|
self.state["navigation"]["frame_idx"] = next_frame
|
|
self.state["navigation"]["last_request"] = iteration
|
|
self.set_state_value("edit", "selected", None)
|
|
|
|
def get_color(self, item):
|
|
""" Return color for selected item """
|
|
return self.state[item]["color"]
|
|
|
|
def get_size(self, item):
|
|
""" Return size for selected item """
|
|
return self.state[item]["size"]
|
|
|
|
def get_frame_scaling(self):
|
|
""" Return frame scaling factor for requested item """
|
|
factors = (1, 1.25, 1.5, 2, 0.5, 0.75)
|
|
idx = self.state["navigation"]["frame-size"] - 1
|
|
return factors[idx]
|
|
|
|
def get_edit_mode(self):
|
|
""" Return text version and border color for edit mode """
|
|
if self.state["edit"]["active"]:
|
|
return "Edit"
|
|
return "View"
|
|
|
|
def get_skip_mode(self):
|
|
""" Return text version of skip mode """
|
|
return self.skip_mode[self.state["navigation"]["skip-mode"]]
|
|
|
|
def get_state_color(self):
|
|
""" Return a color based on current state
|
|
white - View Mode
|
|
yellow - Edit Mode
|
|
red - Unsaved alignments """
|
|
color = (255, 255, 255)
|
|
if self.state["edit"]["updated"]:
|
|
color = (0, 0, 255)
|
|
elif self.state["edit"]["active"]:
|
|
color = (0, 255, 255)
|
|
return color
|
|
|
|
def get_frame_name(self):
|
|
""" Return the current frame number """
|
|
return self.state["navigation"]["frame_name"]
|
|
|
|
def get_selected_face_id(self):
|
|
""" Return the index of the currently selected face """
|
|
try:
|
|
return int(self.state["edit"]["selected"])
|
|
except TypeError:
|
|
return None
|
|
|
|
def redraw(self):
|
|
""" Return whether a redraw is required """
|
|
return self.state["edit"]["redraw"]
|
|
|
|
def set_redraw(self, request):
|
|
""" Turn redraw requirement on or off """
|
|
self.state["edit"]["redraw"] = request
|
|
|
|
|
|
class Help():
|
|
""" Generate and display help in cli and in window """
|
|
def __init__(self, interface):
|
|
logger.debug("Initializing %s: (interface: %s)", self.__class__.__name__, interface)
|
|
self.interface = interface
|
|
self.helptext = self.generate()
|
|
logger.debug("Initialized %s", self.__class__.__name__)
|
|
|
|
def generate(self):
|
|
""" Generate help output """
|
|
logger.debug("Generating help")
|
|
sections = ("navigation", "display", "color", "size", "edit")
|
|
helpout = {section: list() for section in sections}
|
|
helptext = ""
|
|
for key, val in self.interface.controls.items():
|
|
logger.trace("Generating help for:(key: '%s', val: '%s'", key, val)
|
|
help_section = val["args"][0]
|
|
if help_section not in ("navigation", "edit"):
|
|
help_section = val["args"][1]
|
|
key_text = val.get("key_text", None)
|
|
key_text = key_text if key_text else key
|
|
logger.trace("Adding help for:(section: '%s', val: '%s', text: '%s'",
|
|
help_section, val["help"], key_text)
|
|
helpout[help_section].append((val["help"], key_text))
|
|
|
|
helpout["edit"].append(("Bounding Box - Move", "Left Click"))
|
|
helpout["edit"].append(("Bounding Box - Resize", "Middle Click"))
|
|
|
|
for section in sections:
|
|
spacer = "=" * int((40 - len(section)) / 2)
|
|
display = "\n{} {} {}\n".format(spacer, section.upper(), spacer)
|
|
helpsection = sorted(helpout[section])
|
|
if section == "navigation":
|
|
helpsection = sorted(helpout[section], reverse=True)
|
|
display += "\n".join(" - '{}': {}".format(item[1], item[0])
|
|
for item in helpsection)
|
|
|
|
helptext += display
|
|
logger.debug("Added helptext: '%s'", helptext)
|
|
return helptext
|
|
|
|
def render(self):
|
|
""" Render help text to image window """
|
|
# pylint: disable=no-member
|
|
logger.trace("Rendering help text")
|
|
image = self.background()
|
|
display_text = self.helptext + self.compile_status()
|
|
self.text_to_image(image, display_text)
|
|
cv2.namedWindow("Help")
|
|
cv2.imshow("Help", image)
|
|
logger.trace("Rendered help text")
|
|
|
|
def background(self):
|
|
""" Create an image to hold help text """
|
|
# pylint: disable=no-member
|
|
logger.trace("Creating help text canvas")
|
|
height = 880
|
|
width = 480
|
|
image = np.zeros((height, width, 3), np.uint8)
|
|
color = self.interface.get_state_color()
|
|
cv2.rectangle(image, (0, 0), (width - 1, height - 1), color, 2)
|
|
logger.trace("Created help text canvas")
|
|
return image
|
|
|
|
def compile_status(self):
|
|
""" Render the status text """
|
|
logger.trace("Compiling Status text")
|
|
status = "\n=== STATUS\n"
|
|
navigation = self.interface.state["navigation"]
|
|
frame_scale = int(self.interface.get_frame_scaling() * 100)
|
|
status += " File: {}\n".format(self.interface.get_frame_name())
|
|
status += " Frame: {} / {}\n".format(
|
|
navigation["frame_idx"] + 1, navigation["max_frame"] + 1)
|
|
status += " Frame Size: {}%\n".format(frame_scale)
|
|
status += " Skip Mode: {}\n".format(self.interface.get_skip_mode())
|
|
status += " View Mode: {}\n".format(self.interface.get_edit_mode())
|
|
if self.interface.get_selected_face_id() is not None:
|
|
status += " Selected Face Index: {}\n".format(self.interface.get_selected_face_id())
|
|
if self.interface.state["edit"]["updated"]:
|
|
status += " Warning: There are unsaved changes\n"
|
|
|
|
logger.trace("Compiled Status text")
|
|
return status
|
|
|
|
@staticmethod
|
|
def text_to_image(image, display_text):
|
|
""" Write out and format help text to image """
|
|
# pylint: disable=no-member
|
|
logger.trace("Converting help text to image")
|
|
pos_y = 0
|
|
for line in display_text.split("\n"):
|
|
if line.startswith("==="):
|
|
pos_y += 10
|
|
line = line.replace("=", "").strip()
|
|
line = line.replace("- '", "[ ").replace("':", " ]")
|
|
cv2.putText(image, line, (20, pos_y),
|
|
cv2.FONT_HERSHEY_SIMPLEX, 0.43, (255, 255, 255), 1)
|
|
pos_y += 20
|
|
logger.trace("Converted help text to image")
|
|
|
|
|
|
class Manual():
|
|
""" Manually adjust or create landmarks data """
|
|
def __init__(self, alignments, arguments):
|
|
logger.debug("Initializing %s: (alignments: %s, arguments: %s)",
|
|
self.__class__.__name__, alignments, arguments)
|
|
self.arguments = arguments
|
|
self.alignments = alignments
|
|
self.align_eyes = arguments.align_eyes
|
|
self.frames = Frames(arguments.frames_dir)
|
|
self.extracted_faces = None
|
|
self.interface = None
|
|
self.help = None
|
|
self.mouse_handler = None
|
|
logger.debug("Initialized %s", self.__class__.__name__)
|
|
|
|
def process(self):
|
|
""" Process manual extraction """
|
|
legacy = Legacy(self.alignments, self.arguments,
|
|
frames=self.frames, child_process=True)
|
|
legacy.process()
|
|
|
|
logger.info("[MANUAL PROCESSING]") # Tidy up cli output
|
|
self.extracted_faces = ExtractedFaces(self.frames, self.alignments, size=256,
|
|
align_eyes=self.align_eyes)
|
|
self.interface = Interface(self.alignments, self.frames)
|
|
self.help = Help(self.interface)
|
|
self.mouse_handler = MouseHandler(self.interface, self.arguments.loglevel)
|
|
|
|
print(self.help.helptext)
|
|
max_idx = self.frames.count - 1
|
|
self.interface.state["navigation"]["max_frame"] = max_idx
|
|
self.display_frames()
|
|
|
|
def display_frames(self):
|
|
""" Iterate through frames """
|
|
# pylint: disable=no-member
|
|
logger.debug("Display frames")
|
|
is_windows = True if platform.system() == "Windows" else False
|
|
is_conda = True if "conda" in sys.version.lower() else False
|
|
logger.debug("is_windows: %s, is_conda: %s", is_windows, is_conda)
|
|
cv2.namedWindow("Frame")
|
|
cv2.namedWindow("Faces")
|
|
cv2.setMouseCallback('Frame', self.mouse_handler.on_event)
|
|
|
|
frame, faces = self.get_frame()
|
|
press = self.get_keys()
|
|
|
|
while True:
|
|
self.help.render()
|
|
cv2.imshow("Frame", frame)
|
|
cv2.imshow("Faces", faces)
|
|
key = cv2.waitKey(1)
|
|
|
|
if self.window_closed(is_windows, is_conda, key):
|
|
queue_manager.terminate_queues()
|
|
break
|
|
|
|
if key:
|
|
logger.trace("Keypress received: '%s'", key)
|
|
if key in press.keys():
|
|
action = press[key]["action"]
|
|
logger.debug("Keypress action: key: ('%s', action: '%s')", key, action)
|
|
if action == "quit":
|
|
break
|
|
|
|
if press[key].get("key_type") == range:
|
|
args = press[key]["args"] + [chr(key)]
|
|
else:
|
|
args = press[key]["args"]
|
|
action(*args)
|
|
|
|
if not self.interface.redraw():
|
|
continue
|
|
|
|
logger.trace("Redraw requested")
|
|
frame, faces = self.get_frame()
|
|
self.interface.set_redraw(False)
|
|
|
|
cv2.destroyAllWindows()
|
|
|
|
def window_closed(self, is_windows, is_conda, key):
|
|
""" Check whether the window has been closed
|
|
|
|
MS Windows doesn't appear to read the window state property
|
|
properly, so we check for a negative key press.
|
|
|
|
Conda (tested on Windows) doesn't appear to read the window
|
|
state property or negative key press properly, so we arbitrarily
|
|
use another property """
|
|
# pylint: disable=no-member
|
|
logger.trace("Commencing closed window check")
|
|
closed = False
|
|
prop_autosize = cv2.getWindowProperty('Frame', cv2.WND_PROP_AUTOSIZE)
|
|
prop_visible = cv2.getWindowProperty('Frame', cv2.WND_PROP_VISIBLE)
|
|
if self.arguments.disable_monitor:
|
|
closed = False
|
|
elif is_conda and prop_autosize < 1:
|
|
closed = True
|
|
elif is_windows and not is_conda and key == -1:
|
|
closed = True
|
|
elif not is_windows and not is_conda and prop_visible < 1:
|
|
closed = True
|
|
logger.trace("Completed closed window check. Closed is %s", closed)
|
|
if closed:
|
|
logger.debug("Window closed detected")
|
|
return closed
|
|
|
|
def get_keys(self):
|
|
""" Convert keys dict into something useful
|
|
for OpenCV """
|
|
keys = dict()
|
|
for key, val in self.interface.controls.items():
|
|
if val.get("key_type", str) == range:
|
|
for range_key in key:
|
|
keys[ord(str(range_key))] = val
|
|
elif val.get("key_type", str) == ord:
|
|
keys[key] = val
|
|
else:
|
|
keys[ord(key)] = val
|
|
|
|
return keys
|
|
|
|
def get_frame(self):
|
|
""" Compile the frame and get faces """
|
|
image = self.frame_selector()
|
|
frame_name = self.interface.get_frame_name()
|
|
logger.debug("Frame Name: '%s'", frame_name)
|
|
alignments = self.alignments.get_faces_in_frame(frame_name)
|
|
faces_updated = self.interface.state["edit"]["update_faces"]
|
|
logger.debug("Faces Updated: %s", faces_updated)
|
|
self.extracted_faces.get_faces(frame_name)
|
|
roi = [face.original_roi for face in self.extracted_faces.faces]
|
|
|
|
if faces_updated:
|
|
self.interface.state["edit"]["update_faces"] = False
|
|
|
|
frame = FrameDisplay(image, alignments, roi, self.interface).image
|
|
faces = self.set_faces(frame_name).image
|
|
return frame, faces
|
|
|
|
def frame_selector(self):
|
|
""" Return frame at given index """
|
|
navigation = self.interface.state["navigation"]
|
|
frame_list = self.frames.file_list_sorted
|
|
frame = frame_list[navigation["frame_idx"]]["frame_fullname"]
|
|
skip_mode = self.interface.get_skip_mode().lower()
|
|
logger.debug("navigation: %s, frame: '%s', skip_mode: '%s'", navigation, frame, skip_mode)
|
|
|
|
while True:
|
|
if navigation["last_request"] == 0:
|
|
break
|
|
elif navigation["frame_idx"] in (0, navigation["max_frame"]):
|
|
break
|
|
elif skip_mode == "standard":
|
|
break
|
|
elif (skip_mode == "no faces"
|
|
and not self.alignments.frame_has_faces(frame)):
|
|
break
|
|
elif (skip_mode == "multi-faces"
|
|
and self.alignments.frame_has_multiple_faces(frame)):
|
|
break
|
|
elif (skip_mode == "has faces"
|
|
and self.alignments.frame_has_faces(frame)):
|
|
break
|
|
else:
|
|
self.interface.iterate_frame("navigation",
|
|
navigation["last_request"])
|
|
frame = frame_list[navigation["frame_idx"]]["frame_fullname"]
|
|
|
|
image = self.frames.load_image(frame)
|
|
navigation["last_request"] = 0
|
|
navigation["frame_name"] = frame
|
|
return image
|
|
|
|
def set_faces(self, frame):
|
|
""" Pass the current frame faces to faces window """
|
|
faces = self.extracted_faces.get_faces_in_frame(frame)
|
|
landmarks = [{"landmarksXY": face.aligned_landmarks}
|
|
for face in self.extracted_faces.faces]
|
|
return FacesDisplay(faces, landmarks, self.extracted_faces.size, self.interface)
|
|
|
|
|
|
class FrameDisplay():
|
|
"""" Window that holds the frame """
|
|
def __init__(self, image, alignments, roi, interface):
|
|
logger.trace("Initializing %s: (alignments: %s, roi: %s, interface: %s)",
|
|
self.__class__.__name__, alignments, roi, interface)
|
|
self.image = image
|
|
self.roi = roi
|
|
self.alignments = alignments
|
|
self.interface = interface
|
|
self.annotate_frame()
|
|
logger.trace("Initialized %s", self.__class__.__name__)
|
|
|
|
def annotate_frame(self):
|
|
""" Annotate the frame """
|
|
state = self.interface.state
|
|
logger.trace("State: %s", state)
|
|
annotate = Annotate(self.image, self.alignments, self.roi)
|
|
if not state["image"]["display"]:
|
|
annotate.draw_black_image()
|
|
|
|
for item in ("bounding_box", "extract_box", "landmarks", "landmarks_mesh"):
|
|
color = self.interface.get_color(item)
|
|
size = self.interface.get_size(item)
|
|
state[item]["display"] = False if color == 7 else True
|
|
if not state[item]["display"]:
|
|
continue
|
|
logger.trace("Annotating: '%s'", item)
|
|
annotation = getattr(annotate, "draw_{}".format(item))
|
|
annotation(color, size)
|
|
|
|
selected_face = self.interface.get_selected_face_id()
|
|
if (selected_face is not None and
|
|
int(selected_face) < len(self.alignments)):
|
|
annotate.draw_grey_out_faces(selected_face)
|
|
|
|
self.image = self.resize_frame(annotate.image)
|
|
|
|
def resize_frame(self, image):
|
|
""" Set the displayed frame size and add state border"""
|
|
# pylint: disable=no-member
|
|
logger.trace("Resizing frame")
|
|
height, width = image.shape[:2]
|
|
color = self.interface.get_state_color()
|
|
cv2.rectangle(image, (0, 0), (width - 1, height - 1), color, 1)
|
|
scaling = self.interface.get_frame_scaling()
|
|
image = cv2.resize(image, (0, 0), fx=scaling, fy=scaling)
|
|
logger.trace("Resized frame")
|
|
return image
|
|
|
|
|
|
class FacesDisplay():
|
|
""" Window that holds faces thumbnail """
|
|
def __init__(self, extracted_faces, landmarks, size, interface):
|
|
logger.trace("Initializing %s: (extracted_faces: %s, landmarks: %s, size: %s, "
|
|
"interface: %s)", self.__class__.__name__, extracted_faces,
|
|
landmarks, size, interface)
|
|
self.row_length = 4
|
|
self.faces = self.copy_faces(extracted_faces)
|
|
self.roi = self.set_full_roi(size)
|
|
self.landmarks = landmarks
|
|
self.interface = interface
|
|
|
|
self.annotate_faces()
|
|
|
|
self.image = self.build_faces_image(size)
|
|
logger.trace("Initialized %s", self.__class__.__name__)
|
|
|
|
@staticmethod
|
|
def copy_faces(faces):
|
|
""" Copy the extracted faces so as not to save the annotations back """
|
|
return [face.aligned_face.copy() for face in faces]
|
|
|
|
@staticmethod
|
|
def set_full_roi(size):
|
|
""" ROI is the full frame for faces, so set based on size """
|
|
return [np.array([[(0, 0), (0, size - 1), (size - 1, size - 1), (size - 1, 0)]], np.int32)]
|
|
|
|
def annotate_faces(self):
|
|
""" Annotate each of the faces """
|
|
state = self.interface.state
|
|
selected_face = self.interface.get_selected_face_id()
|
|
logger.trace("State: %s, Selected Face ID: %s", state, selected_face)
|
|
for idx, face in enumerate(self.faces):
|
|
annotate = Annotate(face, [self.landmarks[idx]], self.roi)
|
|
if not state["image"]["display"]:
|
|
annotate.draw_black_image()
|
|
|
|
for item in ("landmarks", "landmarks_mesh"):
|
|
if not state[item]["display"]:
|
|
continue
|
|
logger.trace("Annotating: '%s'", item)
|
|
color = self.interface.get_color(item)
|
|
size = self.interface.get_size(item)
|
|
annotation = getattr(annotate, "draw_{}".format(item))
|
|
annotation(color, size)
|
|
|
|
if (selected_face is not None
|
|
and int(selected_face) < len(self.faces)
|
|
and int(selected_face) != idx):
|
|
annotate.draw_grey_out_faces(1)
|
|
|
|
self.faces[idx] = annotate.image
|
|
|
|
def build_faces_image(self, size):
|
|
""" Display associated faces """
|
|
total_faces = len(self.faces)
|
|
logger.trace("Building faces panel. (total_faces: %s", total_faces)
|
|
if not total_faces:
|
|
logger.trace("Returning empty row")
|
|
image = self.build_faces_row(list(), size)
|
|
return image
|
|
total_rows = int(total_faces / self.row_length) + 1
|
|
for idx in range(total_rows):
|
|
logger.trace("Building row %s", idx)
|
|
face_idx = idx * self.row_length
|
|
row_faces = self.faces[face_idx:face_idx + self.row_length]
|
|
if not row_faces:
|
|
break
|
|
row = self.build_faces_row(row_faces, size)
|
|
image = row if idx == 0 else np.concatenate((image, row), axis=0)
|
|
return image
|
|
|
|
def build_faces_row(self, faces, size):
|
|
""" Build a row of 4 faces """
|
|
# pylint: disable=no-member
|
|
logger.trace("Building row for %s faces", len(faces))
|
|
if len(faces) != 4:
|
|
remainder = 4 - (len(faces) % self.row_length)
|
|
for _ in range(remainder):
|
|
faces.append(np.zeros((size, size, 3), np.uint8))
|
|
for idx, face in enumerate(faces):
|
|
color = self.interface.get_state_color()
|
|
cv2.rectangle(face, (0, 0), (size - 1, size - 1),
|
|
color, 1)
|
|
if idx == 0:
|
|
row = face
|
|
else:
|
|
row = np.concatenate((row, face), axis=1)
|
|
return row
|
|
|
|
|
|
class MouseHandler():
|
|
""" Manual Extraction """
|
|
def __init__(self, interface, loglevel):
|
|
logger.debug("Initializing %s: (interface: %s)", self.__class__.__name__, interface)
|
|
self.interface = interface
|
|
self.alignments = interface.alignments
|
|
self.frames = interface.frames
|
|
|
|
self.extractor = dict()
|
|
self.init_extractor(loglevel)
|
|
|
|
self.mouse_state = None
|
|
self.last_move = None
|
|
self.center = None
|
|
self.dims = None
|
|
self.media = {"frame_id": None,
|
|
"image": None,
|
|
"bounding_box": list(),
|
|
"bounding_last": list(),
|
|
"bounding_box_orig": list()}
|
|
logger.debug("Initialized %s", self.__class__.__name__)
|
|
|
|
def init_extractor(self, loglevel):
|
|
""" Initialize FAN """
|
|
logger.debug("Initialize Extractor")
|
|
out_queue = queue_manager.get_queue("out")
|
|
|
|
d_kwargs = {"in_queue": queue_manager.get_queue("in"),
|
|
"out_queue": queue_manager.get_queue("align")}
|
|
a_kwargs = {"in_queue": queue_manager.get_queue("align"),
|
|
"out_queue": out_queue}
|
|
|
|
detector = PluginLoader.get_detector("manual")(loglevel=loglevel)
|
|
detect_process = SpawnProcess(detector.run, **d_kwargs)
|
|
d_event = detect_process.event
|
|
detect_process.start()
|
|
|
|
for plugin in ("fan", "dlib"):
|
|
aligner = PluginLoader.get_aligner(plugin)(loglevel=loglevel)
|
|
align_process = SpawnProcess(aligner.run, **a_kwargs)
|
|
a_event = align_process.event
|
|
align_process.start()
|
|
|
|
# Wait for Aligner to initialize
|
|
# The first ever load of the model for FAN has reportedly taken
|
|
# up to 3-4 minutes, hence high timeout.
|
|
a_event.wait(300)
|
|
if not a_event.is_set():
|
|
if plugin == "fan":
|
|
align_process.join()
|
|
logger.error("Error initializing FAN. Trying Dlib")
|
|
continue
|
|
else:
|
|
raise ValueError("Error inititalizing Aligner")
|
|
if plugin == "dlib":
|
|
break
|
|
|
|
try:
|
|
err = None
|
|
err = out_queue.get(True, 1)
|
|
except QueueEmpty:
|
|
pass
|
|
if not err:
|
|
break
|
|
align_process.join()
|
|
logger.error("Error initializing FAN. Trying Dlib")
|
|
|
|
d_event.wait(10)
|
|
if not d_event.is_set():
|
|
raise ValueError("Error inititalizing Detector")
|
|
|
|
self.extractor["detect"] = detector
|
|
self.extractor["align"] = aligner
|
|
logger.debug("Initialized Extractor")
|
|
|
|
def on_event(self, event, x, y, flags, param): # pylint: disable=unused-argument,invalid-name
|
|
""" Handle the mouse events """
|
|
# pylint: disable=no-member
|
|
if self.interface.get_edit_mode() != "Edit":
|
|
return
|
|
logger.trace("Mouse event: (event: %s, x: %s, y: %s, flags: %s, param: %s",
|
|
event, x, y, flags, param)
|
|
if not self.mouse_state and event not in (cv2.EVENT_LBUTTONDOWN, cv2.EVENT_MBUTTONDOWN):
|
|
return
|
|
|
|
self.initialize()
|
|
|
|
if event in (cv2.EVENT_LBUTTONUP, cv2.EVENT_MBUTTONUP):
|
|
self.mouse_state = None
|
|
self.last_move = None
|
|
elif event == cv2.EVENT_LBUTTONDOWN:
|
|
self.mouse_state = "left"
|
|
self.set_bounding_box(x, y)
|
|
elif event == cv2.EVENT_MBUTTONDOWN:
|
|
self.mouse_state = "middle"
|
|
self.set_bounding_box(x, y)
|
|
elif event == cv2.EVENT_MOUSEMOVE:
|
|
if self.mouse_state == "left":
|
|
self.move_bounding_box(x, y)
|
|
elif self.mouse_state == "middle":
|
|
self.resize_bounding_box(x, y)
|
|
|
|
def initialize(self):
|
|
""" Update changed parameters """
|
|
frame = self.interface.get_frame_name()
|
|
if frame == self.media["frame_id"]:
|
|
return
|
|
logger.debug("Initialize frame: '%s'", frame)
|
|
self.media["frame_id"] = frame
|
|
self.media["image"] = self.frames.load_image(frame)
|
|
self.dims = None
|
|
self.center = None
|
|
self.last_move = None
|
|
self.mouse_state = None
|
|
self.media["bounding_box"] = list()
|
|
self.media["bounding_box_orig"] = list()
|
|
|
|
def set_bounding_box(self, pt_x, pt_y):
|
|
""" Select or create bounding box """
|
|
if self.interface.get_selected_face_id() is None:
|
|
self.check_click_location(pt_x, pt_y)
|
|
|
|
if self.interface.get_selected_face_id() is not None:
|
|
self.dims_from_alignment()
|
|
else:
|
|
self.dims_from_image()
|
|
|
|
self.move_bounding_box(pt_x, pt_y)
|
|
|
|
def check_click_location(self, pt_x, pt_y):
|
|
""" Check whether the point clicked is within an existing
|
|
bounding box and set face_id """
|
|
frame = self.media["frame_id"]
|
|
alignments = self.alignments.get_faces_in_frame(frame)
|
|
|
|
for idx, alignment in enumerate(alignments):
|
|
left = alignment["x"]
|
|
right = alignment["x"] + alignment["w"]
|
|
top = alignment["y"]
|
|
bottom = alignment["y"] + alignment["h"]
|
|
|
|
if left <= pt_x <= right and top <= pt_y <= bottom:
|
|
self.interface.set_state_value("edit", "selected", idx)
|
|
break
|
|
|
|
def dims_from_alignment(self):
|
|
""" Set the height and width of bounding box from alignment """
|
|
frame = self.media["frame_id"]
|
|
face_id = self.interface.get_selected_face_id()
|
|
alignment = self.alignments.get_faces_in_frame(frame)[face_id]
|
|
self.dims = (alignment["w"], alignment["h"])
|
|
|
|
def dims_from_image(self):
|
|
""" Set the height and width of bounding
|
|
box at 10% of longest axis """
|
|
size = max(self.media["image"].shape[:2])
|
|
dim = int(size / 10.00)
|
|
self.dims = (dim, dim)
|
|
|
|
def bounding_from_center(self):
|
|
""" Get bounding X Y from center """
|
|
pt_x, pt_y = self.center
|
|
width, height = self.dims
|
|
scale = self.interface.get_frame_scaling()
|
|
self.media["bounding_box"] = [int((pt_x / scale) - width / 2),
|
|
int((pt_y / scale) - height / 2),
|
|
int((pt_x / scale) + width / 2),
|
|
int((pt_y / scale) + height / 2)]
|
|
|
|
def move_bounding_box(self, pt_x, pt_y):
|
|
""" Move the bounding box """
|
|
self.center = (pt_x, pt_y)
|
|
self.bounding_from_center()
|
|
self.update_landmarks()
|
|
|
|
def resize_bounding_box(self, pt_x, pt_y):
|
|
""" Resize the bounding box """
|
|
scale = self.interface.get_frame_scaling()
|
|
|
|
if not self.last_move:
|
|
self.last_move = (pt_x, pt_y)
|
|
self.media["bounding_box_orig"] = self.media["bounding_box"]
|
|
|
|
move_x = int(pt_x - self.last_move[0])
|
|
move_y = int(self.last_move[1] - pt_y)
|
|
|
|
original = self.media["bounding_box_orig"]
|
|
updated = self.media["bounding_box"]
|
|
|
|
minsize = int(10 / scale)
|
|
center = (int(self.center[0] / scale), int(self.center[1] / scale))
|
|
updated[0] = min(center[0] - minsize, original[0] - move_x)
|
|
updated[1] = min(center[1] - minsize, original[1] - move_y)
|
|
updated[2] = max(center[0] + minsize, original[2] + move_x)
|
|
updated[3] = max(center[1] + minsize, original[3] + move_y)
|
|
self.update_landmarks()
|
|
self.last_move = (pt_x, pt_y)
|
|
|
|
def update_landmarks(self):
|
|
""" Update the landmarks """
|
|
queue_manager.get_queue("in").put({"image": self.media["image"],
|
|
"filename": self.media["frame_id"],
|
|
"face": self.media["bounding_box"]})
|
|
landmarks = queue_manager.get_queue("out").get()
|
|
|
|
if isinstance(landmarks, dict) and landmarks.get("exception"):
|
|
cv2.destroyAllWindows() # pylint: disable=no-member
|
|
pid = landmarks["exception"][0]
|
|
t_back = landmarks["exception"][1].getvalue()
|
|
err = "Error in child process {}. {}".format(pid, t_back)
|
|
raise Exception(err)
|
|
if landmarks == "EOF":
|
|
exit(0)
|
|
|
|
alignment = self.extracted_to_alignment((landmarks["detected_faces"][0],
|
|
landmarks["landmarks"][0]))
|
|
frame = self.media["frame_id"]
|
|
|
|
if self.interface.get_selected_face_id() is None:
|
|
idx = self.alignments.add_face(frame, alignment)
|
|
self.interface.set_state_value("edit", "selected", idx)
|
|
else:
|
|
self.alignments.update_face(frame,
|
|
self.interface.get_selected_face_id(),
|
|
alignment)
|
|
self.interface.set_redraw(True)
|
|
|
|
self.interface.state["edit"]["updated"] = True
|
|
self.interface.state["edit"]["update_faces"] = True
|
|
|
|
@staticmethod
|
|
def extracted_to_alignment(extract_data):
|
|
""" Convert Extracted Tuple to Alignments data """
|
|
alignment = dict()
|
|
d_rect, landmarks = extract_data
|
|
alignment["x"] = d_rect.left()
|
|
alignment["w"] = d_rect.right() - d_rect.left()
|
|
alignment["y"] = d_rect.top()
|
|
alignment["h"] = d_rect.bottom() - d_rect.top()
|
|
alignment["landmarksXY"] = landmarks
|
|
return alignment
|