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
358 lines
15 KiB
Python
358 lines
15 KiB
Python
#!/usr/bin/env python3
|
|
""" Masked converter for faceswap.py
|
|
Based on: https://gist.github.com/anonymous/d3815aba83a8f79779451262599b0955
|
|
found on https://www.reddit.com/r/deepfakes/ """
|
|
|
|
import logging
|
|
import cv2
|
|
import numpy as np
|
|
from lib.model.masks import dfl_full
|
|
|
|
logger = logging.getLogger(__name__) # pylint: disable=invalid-name
|
|
|
|
|
|
class Convert():
|
|
""" Swap a source face with a target """
|
|
def __init__(self, encoder, model, arguments):
|
|
logger.debug("Initializing %s: (encoder: '%s', model: %s, arguments: %s",
|
|
self.__class__.__name__, encoder, model, arguments)
|
|
self.encoder = encoder
|
|
self.args = arguments
|
|
self.input_size = model.input_shape[0]
|
|
self.training_size = model.state.training_size
|
|
self.training_coverage_ratio = model.training_opts["coverage_ratio"]
|
|
self.input_mask_shape = model.state.mask_shapes[0] if model.state.mask_shapes else None
|
|
|
|
self.crop = None
|
|
self.mask = None
|
|
logger.debug("Initialized %s", self.__class__.__name__)
|
|
|
|
def patch_image(self, image, detected_face):
|
|
""" Patch the image """
|
|
logger.trace("Patching image")
|
|
image = image.astype('float32')
|
|
image_size = (image.shape[1], image.shape[0])
|
|
coverage = int(self.training_coverage_ratio * self.training_size)
|
|
padding = (self.training_size - coverage) // 2
|
|
logger.trace("coverage: %s, padding: %s", coverage, padding)
|
|
|
|
self.crop = slice(padding, self.training_size - padding)
|
|
if not self.mask: # Init the mask on first image
|
|
self.mask = Mask(self.args.mask_type, self.training_size, padding, self.crop)
|
|
|
|
detected_face.load_aligned(image, size=self.training_size, align_eyes=False)
|
|
new_image = self.get_new_image(image, detected_face, coverage, image_size)
|
|
image_mask = self.get_image_mask(detected_face, image_size)
|
|
patched_face = self.apply_fixes(image,
|
|
new_image,
|
|
image_mask,
|
|
image_size)
|
|
|
|
logger.trace("Patched image")
|
|
return patched_face
|
|
|
|
def get_new_image(self, image, detected_face, coverage, image_size):
|
|
""" Get the new face from the predictor """
|
|
logger.trace("coverage: %s", coverage)
|
|
src_face = detected_face.aligned_face
|
|
coverage_face = src_face[self.crop, self.crop]
|
|
coverage_face = cv2.resize(coverage_face, # pylint: disable=no-member
|
|
(self.input_size, self.input_size),
|
|
interpolation=cv2.INTER_AREA) # pylint: disable=no-member
|
|
coverage_face = np.expand_dims(coverage_face, 0)
|
|
np.clip(coverage_face / 255.0, 0.0, 1.0, out=coverage_face)
|
|
|
|
if self.input_mask_shape:
|
|
mask = np.zeros(self.input_mask_shape, np.float32)
|
|
mask = np.expand_dims(mask, 0)
|
|
feed = [coverage_face, mask]
|
|
else:
|
|
feed = [coverage_face]
|
|
logger.trace("Input shapes: %s", [item.shape for item in feed])
|
|
new_face = self.encoder(feed)[0]
|
|
new_face = new_face.squeeze()
|
|
logger.trace("Output shape: %s", new_face.shape)
|
|
|
|
new_face = cv2.resize(new_face, # pylint: disable=no-member
|
|
(coverage, coverage),
|
|
interpolation=cv2.INTER_CUBIC) # pylint: disable=no-member
|
|
np.clip(new_face * 255.0, 0.0, 255.0, out=new_face)
|
|
src_face[self.crop, self.crop] = new_face
|
|
background = image.copy()
|
|
interpolator = detected_face.adjusted_interpolators[1]
|
|
new_image = cv2.warpAffine( # pylint: disable=no-member
|
|
src_face,
|
|
detected_face.adjusted_matrix,
|
|
image_size,
|
|
background,
|
|
flags=cv2.WARP_INVERSE_MAP | interpolator, # pylint: disable=no-member
|
|
borderMode=cv2.BORDER_TRANSPARENT) # pylint: disable=no-member
|
|
return new_image
|
|
|
|
def get_image_mask(self, detected_face, image_size):
|
|
""" Get the image mask """
|
|
mask = self.mask.get_mask(detected_face, image_size)
|
|
if self.args.erosion_size != 0:
|
|
kwargs = {'src': mask,
|
|
'kernel': self.set_erosion_kernel(mask),
|
|
'iterations': 1}
|
|
if self.args.erosion_size > 0:
|
|
mask = cv2.erode(**kwargs) # pylint: disable=no-member
|
|
else:
|
|
mask = cv2.dilate(**kwargs) # pylint: disable=no-member
|
|
|
|
if self.args.blur_size != 0:
|
|
blur_size = self.set_blur_size(mask)
|
|
mask = cv2.blur(mask, (blur_size, blur_size)) # pylint: disable=no-member
|
|
|
|
return np.clip(mask, 0.0, 1.0, out=mask)
|
|
|
|
def set_erosion_kernel(self, mask):
|
|
""" Set the erosion kernel """
|
|
erosion_ratio = self.args.erosion_size / 100
|
|
mask_radius = np.sqrt(np.sum(mask)) / 2
|
|
percent_erode = max(1, int(abs(erosion_ratio * mask_radius)))
|
|
erosion_kernel = cv2.getStructuringElement( # pylint: disable=no-member
|
|
cv2.MORPH_ELLIPSE, # pylint: disable=no-member
|
|
(percent_erode, percent_erode))
|
|
logger.trace("erosion_kernel shape: %s", erosion_kernel.shape)
|
|
return erosion_kernel
|
|
|
|
def set_blur_size(self, mask):
|
|
""" Set the blur size to absolute or percentage """
|
|
blur_ratio = self.args.blur_size / 100
|
|
mask_radius = np.sqrt(np.sum(mask)) / 2
|
|
blur_size = int(max(1, blur_ratio * mask_radius))
|
|
logger.trace("blur_size: %s", blur_size)
|
|
return blur_size
|
|
|
|
def apply_fixes(self, frame, new_image, image_mask, image_size):
|
|
""" Apply fixes """
|
|
masked = new_image # * image_mask
|
|
|
|
if self.args.draw_transparent:
|
|
alpha = np.full((image_size[1], image_size[0], 1), 255.0, dtype='float32')
|
|
new_image = np.concatenate(new_image, alpha, axis=2)
|
|
image_mask = np.concatenate(image_mask, alpha, axis=2)
|
|
frame = np.concatenate(frame, alpha, axis=2)
|
|
|
|
if self.args.sharpen_image is not None:
|
|
np.clip(masked, 0.0, 255.0, out=masked)
|
|
if self.args.sharpen_image == "box_filter":
|
|
kernel = np.ones((3, 3)) * (-1)
|
|
kernel[1, 1] = 9
|
|
masked = cv2.filter2D(masked, -1, kernel) # pylint: disable=no-member
|
|
elif self.args.sharpen_image == "gaussian_filter":
|
|
blur = cv2.GaussianBlur(masked, (0, 0), 3.0) # pylint: disable=no-member
|
|
masked = cv2.addWeighted(masked, # pylint: disable=no-member
|
|
1.5,
|
|
blur,
|
|
-0.5,
|
|
0,
|
|
masked)
|
|
|
|
if self.args.avg_color_adjust:
|
|
for _ in [0, 1]:
|
|
np.clip(masked, 0.0, 255.0, out=masked)
|
|
diff = frame - masked
|
|
avg_diff = np.sum(diff * image_mask, axis=(0, 1))
|
|
adjustment = avg_diff / np.sum(image_mask, axis=(0, 1))
|
|
masked = masked + adjustment
|
|
|
|
if self.args.match_histogram:
|
|
np.clip(masked, 0.0, 255.0, out=masked)
|
|
masked = self.color_hist_match(masked, frame, image_mask)
|
|
|
|
if self.args.seamless_clone and not self.args.draw_transparent:
|
|
h, w, _ = frame.shape
|
|
h = h // 2
|
|
w = w // 2
|
|
|
|
y_indices, x_indices, _ = np.nonzero(image_mask)
|
|
y_crop = slice(np.min(y_indices), np.max(y_indices))
|
|
x_crop = slice(np.min(x_indices), np.max(x_indices))
|
|
y_center = int(np.rint((np.max(y_indices) + np.min(y_indices)) / 2) + h)
|
|
x_center = int(np.rint((np.max(x_indices) + np.min(x_indices)) / 2) + w)
|
|
|
|
'''
|
|
# test with average of centroid rather than the h /2 , w/2 center
|
|
y_center = int(np.rint(np.average(y_indices) + h)
|
|
x_center = int(np.rint(np.average(x_indices) + w)
|
|
'''
|
|
|
|
insertion = np.rint(masked[y_crop, x_crop, :]).astype('uint8')
|
|
insertion_mask = image_mask[y_crop, x_crop, :]
|
|
insertion_mask[insertion_mask != 0] = 255
|
|
insertion_mask = insertion_mask.astype('uint8')
|
|
|
|
prior = np.pad(frame, ((h, h), (w, w), (0, 0)), 'constant').astype('uint8')
|
|
|
|
blended = cv2.seamlessClone(insertion, # pylint: disable=no-member
|
|
prior,
|
|
insertion_mask,
|
|
(x_center, y_center),
|
|
cv2.NORMAL_CLONE) # pylint: disable=no-member
|
|
blended = blended[h:-h, w:-w, :]
|
|
|
|
else:
|
|
foreground = masked * image_mask
|
|
background = frame * (1.0 - image_mask)
|
|
blended = foreground + background
|
|
|
|
np.clip(blended, 0.0, 255.0, out=blended)
|
|
|
|
return np.rint(blended).astype('uint8')
|
|
|
|
def color_hist_match(self, new, frame, image_mask):
|
|
for channel in [0, 1, 2]:
|
|
new[:, :, channel] = self.hist_match(new[:, :, channel],
|
|
frame[:, :, channel],
|
|
image_mask[:, :, channel])
|
|
# source = np.stack([self.hist_match(source[:,:,c], target[:,:,c],image_mask[:,:,c])
|
|
# for c in [0,1,2]],
|
|
# axis=2)
|
|
return new
|
|
|
|
def hist_match(self, new, frame, image_mask):
|
|
|
|
mask_indices = np.nonzero(image_mask)
|
|
if len(mask_indices[0]) == 0:
|
|
return new
|
|
|
|
m_new = new[mask_indices].ravel()
|
|
m_frame = frame[mask_indices].ravel()
|
|
s_values, bin_idx, s_counts = np.unique(m_new, return_inverse=True, return_counts=True)
|
|
t_values, t_counts = np.unique(m_frame, return_counts=True)
|
|
s_quants = np.cumsum(s_counts, dtype='float32')
|
|
t_quants = np.cumsum(t_counts, dtype='float32')
|
|
s_quants /= s_quants[-1] # cdf
|
|
t_quants /= t_quants[-1] # cdf
|
|
interp_s_values = np.interp(s_quants, t_quants, t_values)
|
|
new.put(mask_indices, interp_s_values[bin_idx])
|
|
|
|
'''
|
|
bins = np.arange(256)
|
|
template_CDF, _ = np.histogram(m_frame, bins=bins, density=True)
|
|
flat_new_image = np.interp(m_source.ravel(), bins[:-1], template_CDF) * 255.0
|
|
return flat_new_image.reshape(m_source.shape) * 255.0
|
|
'''
|
|
|
|
return new
|
|
|
|
|
|
class Mask():
|
|
""" Return the requested mask """
|
|
|
|
def __init__(self, mask_type, training_size, padding, crop):
|
|
""" Set requested mask """
|
|
logger.debug("Initializing %s: (mask_type: '%s', training_size: %s, padding: %s)",
|
|
self.__class__.__name__, mask_type, training_size, padding)
|
|
|
|
self.training_size = training_size
|
|
self.padding = padding
|
|
self.mask_type = mask_type
|
|
self.crop = crop
|
|
|
|
logger.debug("Initialized %s", self.__class__.__name__)
|
|
|
|
def get_mask(self, detected_face, image_size):
|
|
""" Return a face mask """
|
|
kwargs = {"matrix": detected_face.adjusted_matrix,
|
|
"interpolators": detected_face.adjusted_interpolators,
|
|
"landmarks": detected_face.landmarks_as_xy,
|
|
"image_size": image_size}
|
|
logger.trace("kwargs: %s", kwargs)
|
|
mask = getattr(self, self.mask_type)(**kwargs)
|
|
mask = self.finalize_mask(mask)
|
|
logger.trace("mask shape: %s", mask.shape)
|
|
return mask
|
|
|
|
def cnn(self, **kwargs):
|
|
""" CNN Mask """
|
|
# Insert FCN-VGG16 segmentation mask model here
|
|
logger.info("cnn not yet implemented, using facehull instead")
|
|
return self.facehull(**kwargs)
|
|
|
|
def smoothed(self, **kwargs):
|
|
""" Smoothed Mask """
|
|
logger.trace("Getting mask")
|
|
interpolator = kwargs["interpolators"][1]
|
|
ones = np.zeros((self.training_size, self.training_size, 3), dtype='float32')
|
|
# area = self.padding + (self.training_size - 2 * self.padding) // 15
|
|
# central_core = slice(area, -area)
|
|
ones[self.crop, self.crop] = 1.0
|
|
ones = cv2.GaussianBlur(ones, (25, 25), 10) # pylint: disable=no-member
|
|
|
|
mask = np.zeros((kwargs["image_size"][1], kwargs["image_size"][0], 3), dtype='float32')
|
|
cv2.warpAffine(ones, # pylint: disable=no-member
|
|
kwargs["matrix"],
|
|
kwargs["image_size"],
|
|
mask,
|
|
flags=cv2.WARP_INVERSE_MAP | interpolator, # pylint: disable=no-member
|
|
borderMode=cv2.BORDER_CONSTANT, # pylint: disable=no-member
|
|
borderValue=0.0)
|
|
return mask
|
|
|
|
def rect(self, **kwargs):
|
|
""" Rect Mask """
|
|
logger.trace("Getting mask")
|
|
interpolator = kwargs["interpolators"][1]
|
|
ones = np.zeros((self.training_size, self.training_size, 3), dtype='float32')
|
|
mask = np.zeros((kwargs["image_size"][1], kwargs["image_size"][0], 3), dtype='float32')
|
|
# central_core = slice(self.padding, -self.padding)
|
|
ones[self.crop, self.crop] = 1.0
|
|
cv2.warpAffine(ones, # pylint: disable=no-member
|
|
kwargs["matrix"],
|
|
kwargs["image_size"],
|
|
mask,
|
|
flags=cv2.WARP_INVERSE_MAP | interpolator, # pylint: disable=no-member
|
|
borderMode=cv2.BORDER_CONSTANT, # pylint: disable=no-member
|
|
borderValue=0.0)
|
|
return mask
|
|
|
|
def dfl(self, **kwargs):
|
|
""" DFaker Mask """
|
|
logger.trace("Getting mask")
|
|
dummy = np.zeros((kwargs["image_size"][1], kwargs["image_size"][0], 3), dtype='float32')
|
|
mask = dfl_full(kwargs["landmarks"], dummy, channels=3)
|
|
return mask
|
|
|
|
def facehull(self, **kwargs):
|
|
""" Facehull Mask """
|
|
logger.trace("Getting mask")
|
|
mask = np.zeros((kwargs["image_size"][1], kwargs["image_size"][0], 3), dtype='float32')
|
|
hull = cv2.convexHull( # pylint: disable=no-member
|
|
np.array(kwargs["landmarks"]).reshape((-1, 2)))
|
|
cv2.fillConvexPoly(mask, # pylint: disable=no-member
|
|
hull,
|
|
(1.0, 1.0, 1.0),
|
|
lineType=cv2.LINE_AA) # pylint: disable=no-member
|
|
return mask
|
|
|
|
def facehull_rect(self, **kwargs):
|
|
""" Facehull Rect Mask """
|
|
logger.trace("Getting mask")
|
|
mask = self.rect(**kwargs)
|
|
hull_mask = self.facehull(**kwargs)
|
|
mask *= hull_mask
|
|
return mask
|
|
|
|
def ellipse(self, **kwargs):
|
|
""" Ellipse Mask """
|
|
logger.trace("Getting mask")
|
|
mask = np.zeros((kwargs["image_size"][1], kwargs["image_size"][0], 3), dtype='float32')
|
|
ell = cv2.fitEllipse( # pylint: disable=no-member
|
|
np.array(kwargs["landmarks"]).reshape((-1, 2)))
|
|
cv2.ellipse(mask, # pylint: disable=no-member
|
|
box=ell,
|
|
color=(1.0, 1.0, 1.0),
|
|
thickness=-1)
|
|
return mask
|
|
|
|
@staticmethod
|
|
def finalize_mask(mask):
|
|
""" Finalize the mask """
|
|
logger.trace("Finalizing mask")
|
|
np.nan_to_num(mask, copy=False)
|
|
np.clip(mask, 0.0, 1.0, out=mask)
|
|
return mask
|