mirror of
https://github.com/zebrajr/faceswap.git
synced 2025-12-06 00:20:09 +01: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
324 lines
12 KiB
Python
324 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
""" Base class for Face Detector plugins
|
|
Plugins should inherit from this class
|
|
|
|
See the override methods for which methods are
|
|
required.
|
|
|
|
For each source frame, the plugin must pass a dict to finalize containing:
|
|
{"filename": <filename of source frame>,
|
|
"image": <source image>,
|
|
"detected_faces": <list of dlib.rectangles>}
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
import traceback
|
|
from io import StringIO
|
|
|
|
import cv2
|
|
import dlib
|
|
from math import sqrt
|
|
|
|
from lib.gpu_stats import GPUStats
|
|
from lib.utils import rotate_landmarks
|
|
from plugins.extract._config import Config
|
|
|
|
logger = logging.getLogger(__name__) # pylint: disable=invalid-name
|
|
|
|
|
|
def get_config(plugin_name):
|
|
""" Return the config for the requested model """
|
|
return Config(plugin_name).config_dict
|
|
|
|
|
|
class Detector():
|
|
""" Detector object """
|
|
def __init__(self, loglevel, rotation=None):
|
|
logger.debug("Initializing %s: (rotation: %s)", self.__class__.__name__, rotation)
|
|
self.config = get_config(".".join(self.__module__.split(".")[-2:]))
|
|
self.loglevel = loglevel
|
|
self.cachepath = os.path.join(os.path.dirname(__file__), ".cache")
|
|
self.rotation = self.get_rotation_angles(rotation)
|
|
self.parent_is_pool = False
|
|
self.init = None
|
|
|
|
# The input and output queues for the plugin.
|
|
# See lib.queue_manager.QueueManager for getting queues
|
|
self.queues = {"in": None, "out": None}
|
|
|
|
# Path to model if required
|
|
self.model_path = self.set_model_path()
|
|
|
|
# Target image size for passing images through the detector
|
|
# Set to tuple of dimensions (x, y) or int of pixel count
|
|
self.target = None
|
|
|
|
# Approximate VRAM used for the set target. Used to calculate
|
|
# how many parallel processes / batches can be run.
|
|
# Be conservative to avoid OOM.
|
|
self.vram = None
|
|
|
|
# For detectors that support batching, this should be set to
|
|
# the calculated batch size that the amount of available VRAM
|
|
# will support. It is also used for holding the number of threads/
|
|
# processes for parallel processing plugins
|
|
self.batch_size = 1
|
|
logger.debug("Initialized _base %s", self.__class__.__name__)
|
|
|
|
# <<< OVERRIDE METHODS >>> #
|
|
# These methods must be overriden when creating a plugin
|
|
@staticmethod
|
|
def set_model_path():
|
|
""" path to data file/models
|
|
override for specific detector """
|
|
raise NotImplementedError()
|
|
|
|
def initialize(self, *args, **kwargs):
|
|
""" Inititalize the detector
|
|
Tasks to be run before any detection is performed.
|
|
Override for specific detector """
|
|
logger_init = kwargs["log_init"]
|
|
log_queue = kwargs["log_queue"]
|
|
logger_init(self.loglevel, log_queue)
|
|
logger.debug("initialize %s (PID: %s, args: %s, kwargs: %s)",
|
|
self.__class__.__name__, os.getpid(), args, kwargs)
|
|
self.init = kwargs.get("event", False)
|
|
self.queues["in"] = kwargs["in_queue"]
|
|
self.queues["out"] = kwargs["out_queue"]
|
|
|
|
def detect_faces(self, *args, **kwargs):
|
|
""" Detect faces in rgb image
|
|
Override for specific detector
|
|
Must return a list of dlib rects"""
|
|
try:
|
|
if not self.init:
|
|
self.initialize(*args, **kwargs)
|
|
except ValueError as err:
|
|
logger.error(err)
|
|
exit(1)
|
|
logger.debug("Detecting Faces (args: %s, kwargs: %s)", args, kwargs)
|
|
|
|
# <<< DETECTION WRAPPER >>> #
|
|
def run(self, *args, **kwargs):
|
|
""" Parent detect process.
|
|
This should always be called as the entry point so exceptions
|
|
are passed back to parent.
|
|
Do not override """
|
|
try:
|
|
self.detect_faces(*args, **kwargs)
|
|
except Exception: # pylint: disable=broad-except
|
|
logger.error("Caught exception in child process: %s", os.getpid())
|
|
# Display traceback if in initialization stage
|
|
if not self.init.is_set():
|
|
logger.exception("Traceback:")
|
|
tb_buffer = StringIO()
|
|
traceback.print_exc(file=tb_buffer)
|
|
logger.trace(tb_buffer.getvalue())
|
|
exception = {"exception": (os.getpid(), tb_buffer)}
|
|
self.queues["out"].put(exception)
|
|
exit(1)
|
|
|
|
# <<< FINALIZE METHODS>>> #
|
|
def finalize(self, output):
|
|
""" This should be called as the final task of each plugin
|
|
Performs fianl processing and puts to the out queue """
|
|
if isinstance(output, dict):
|
|
logger.trace("Item out: %s", {key: val
|
|
for key, val in output.items()
|
|
if key != "image"})
|
|
else:
|
|
logger.trace("Item out: %s", output)
|
|
self.queues["out"].put(output)
|
|
|
|
# <<< DETECTION IMAGE COMPILATION METHODS >>> #
|
|
def compile_detection_image(self, image, is_square, scale_up):
|
|
""" Compile the detection image """
|
|
scale = self.set_scale(image, is_square=is_square, scale_up=scale_up)
|
|
return [self.set_detect_image(image, scale), scale]
|
|
|
|
def set_scale(self, image, is_square=False, scale_up=False):
|
|
""" Set the scale factor for incoming image """
|
|
height, width = image.shape[:2]
|
|
if is_square:
|
|
if isinstance(self.target, int):
|
|
dims = (self.target ** 0.5, self.target ** 0.5)
|
|
self.target = dims
|
|
source = max(height, width)
|
|
target = max(self.target)
|
|
else:
|
|
if isinstance(self.target, tuple):
|
|
self.target = self.target[0] * self.target[1]
|
|
source = width * height
|
|
target = self.target
|
|
|
|
if scale_up or target < source:
|
|
scale = sqrt(target / source)
|
|
else:
|
|
scale = 1.0
|
|
logger.trace("Detector scale: %s", scale)
|
|
|
|
return scale
|
|
|
|
def set_detect_image(self, input_image, scale):
|
|
""" Convert the image to RGB and scale """
|
|
# pylint: disable=no-member
|
|
image = input_image[:, :, ::-1].copy()
|
|
if scale == 1.0:
|
|
return image
|
|
|
|
height, width = image.shape[:2]
|
|
interpln = cv2.INTER_LINEAR if scale > 1.0 else cv2.INTER_AREA
|
|
dims = (int(width * scale), int(height * scale))
|
|
|
|
if scale < 1.0:
|
|
logger.verbose("Resizing image from %sx%s to %s.",
|
|
width, height, "x".join(str(i) for i in dims))
|
|
|
|
image = cv2.resize(image, dims, interpolation=interpln)
|
|
return image
|
|
|
|
# <<< IMAGE ROTATION METHODS >>> #
|
|
@staticmethod
|
|
def get_rotation_angles(rotation):
|
|
""" Set the rotation angles. Includes backwards compatibility for the
|
|
'on' and 'off' options:
|
|
- 'on' - increment 90 degrees
|
|
- 'off' - disable
|
|
- 0 is prepended to the list, as whatever happens, we want to
|
|
scan the image in it's upright state """
|
|
rotation_angles = [0]
|
|
|
|
if not rotation or rotation.lower() == "off":
|
|
logger.debug("Not setting rotation angles")
|
|
return rotation_angles
|
|
|
|
if rotation.lower() == "on":
|
|
rotation_angles.extend(range(90, 360, 90))
|
|
else:
|
|
passed_angles = [int(angle)
|
|
for angle in rotation.split(",")]
|
|
if len(passed_angles) == 1:
|
|
rotation_step_size = passed_angles[0]
|
|
rotation_angles.extend(range(rotation_step_size,
|
|
360,
|
|
rotation_step_size))
|
|
elif len(passed_angles) > 1:
|
|
rotation_angles.extend(passed_angles)
|
|
|
|
logger.debug("Rotation Angles: %s", rotation_angles)
|
|
return rotation_angles
|
|
|
|
def rotate_image(self, image, angle):
|
|
""" Rotate the image by given angle and return
|
|
Image with rotation matrix """
|
|
if angle == 0:
|
|
return image, None
|
|
return self.rotate_image_by_angle(image, angle)
|
|
|
|
@staticmethod
|
|
def rotate_rect(d_rect, rotation_matrix):
|
|
""" Rotate a dlib rect based on the rotation_matrix"""
|
|
logger.trace("Rotating d_rectangle")
|
|
d_rect = rotate_landmarks(d_rect, rotation_matrix)
|
|
return d_rect
|
|
|
|
@staticmethod
|
|
def rotate_image_by_angle(image, angle,
|
|
rotated_width=None, rotated_height=None):
|
|
""" Rotate an image by a given angle.
|
|
From: https://stackoverflow.com/questions/22041699 """
|
|
|
|
logger.trace("Rotating image: (angle: %s, rotated_width: %s, rotated_height: %s)",
|
|
angle, rotated_width, rotated_height)
|
|
height, width = image.shape[:2]
|
|
image_center = (width/2, height/2)
|
|
rotation_matrix = cv2.getRotationMatrix2D( # pylint: disable=no-member
|
|
image_center, -1.*angle, 1.)
|
|
if rotated_width is None or rotated_height is None:
|
|
abs_cos = abs(rotation_matrix[0, 0])
|
|
abs_sin = abs(rotation_matrix[0, 1])
|
|
if rotated_width is None:
|
|
rotated_width = int(height*abs_sin + width*abs_cos)
|
|
if rotated_height is None:
|
|
rotated_height = int(height*abs_cos + width*abs_sin)
|
|
rotation_matrix[0, 2] += rotated_width/2 - image_center[0]
|
|
rotation_matrix[1, 2] += rotated_height/2 - image_center[1]
|
|
logger.trace("Rotated image: (rotation_matrix: %s", rotation_matrix)
|
|
return (cv2.warpAffine(image, # pylint: disable=no-member
|
|
rotation_matrix,
|
|
(rotated_width, rotated_height)),
|
|
rotation_matrix)
|
|
|
|
# << QUEUE METHODS >> #
|
|
def get_item(self):
|
|
""" Yield one item from the queue """
|
|
item = self.queues["in"].get()
|
|
if isinstance(item, dict):
|
|
logger.trace("Item in: %s", item["filename"])
|
|
else:
|
|
logger.trace("Item in: %s", item)
|
|
if item == "EOF":
|
|
logger.debug("In Queue Exhausted")
|
|
# Re-put EOF into queue for other threads
|
|
self.queues["in"].put(item)
|
|
return item
|
|
|
|
def get_batch(self):
|
|
""" Get items from the queue in batches of
|
|
self.batch_size
|
|
|
|
First item in output tuple indicates whether the
|
|
queue is exhausted.
|
|
Second item is the batch
|
|
|
|
Remember to put "EOF" to the out queue after processing
|
|
the final batch """
|
|
exhausted = False
|
|
batch = list()
|
|
for _ in range(self.batch_size):
|
|
item = self.get_item()
|
|
if item == "EOF":
|
|
exhausted = True
|
|
break
|
|
batch.append(item)
|
|
logger.trace("Returning batch size: %s", len(batch))
|
|
return (exhausted, batch)
|
|
|
|
# <<< DLIB RECTANGLE METHODS >>> #
|
|
@staticmethod
|
|
def is_mmod_rectangle(d_rectangle):
|
|
""" Return whether the passed in object is
|
|
a dlib.mmod_rectangle """
|
|
return isinstance(
|
|
d_rectangle,
|
|
dlib.mmod_rectangle) # pylint: disable=c-extension-no-member
|
|
|
|
def convert_to_dlib_rectangle(self, d_rect):
|
|
""" Convert detected mmod_rects to dlib_rectangle """
|
|
if self.is_mmod_rectangle(d_rect):
|
|
return d_rect.rect
|
|
return d_rect
|
|
|
|
# <<< MISC METHODS >>> #
|
|
@staticmethod
|
|
def get_vram_free():
|
|
""" Return total free VRAM on largest card """
|
|
stats = GPUStats()
|
|
vram = stats.get_card_most_free()
|
|
logger.verbose("Using device %s with %sMB free of %sMB",
|
|
vram["device"],
|
|
int(vram["free"]),
|
|
int(vram["total"]))
|
|
return int(vram["free"])
|
|
|
|
@staticmethod
|
|
def set_predetected(width, height):
|
|
""" Set a dlib rectangle for predetected faces """
|
|
# Predetected_face is used for sort tool.
|
|
# Landmarks should not be extracted again from predetected faces,
|
|
# because face data is lost, resulting in a large variance
|
|
# against extract from original image
|
|
logger.debug("Setting predetected face")
|
|
return [dlib.rectangle(0, 0, width, height)] # pylint: disable=c-extension-no-member
|