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
330 lines
13 KiB
Python
330 lines
13 KiB
Python
#!/usr/bin python3
|
|
""" The script to run the convert process of faceswap """
|
|
|
|
import logging
|
|
import re
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import cv2
|
|
from tqdm import tqdm
|
|
|
|
from scripts.fsmedia import Alignments, Images, PostProcess, Utils
|
|
from lib.faces_detect import DetectedFace
|
|
from lib.multithreading import BackgroundGenerator, SpawnProcess
|
|
from lib.queue_manager import queue_manager
|
|
from lib.utils import get_folder, get_image_paths, hash_image_file
|
|
from plugins.plugin_loader import PluginLoader
|
|
|
|
logger = logging.getLogger(__name__) # pylint: disable=invalid-name
|
|
|
|
|
|
class Convert():
|
|
""" The convert process. """
|
|
def __init__(self, arguments):
|
|
logger.debug("Initializing %s: (args: %s)", self.__class__.__name__, arguments)
|
|
self.args = arguments
|
|
self.output_dir = get_folder(self.args.output_dir)
|
|
self.extract_faces = False
|
|
self.faces_count = 0
|
|
|
|
self.images = Images(self.args)
|
|
self.alignments = Alignments(self.args, False, self.images.is_video)
|
|
|
|
# Update Legacy alignments
|
|
Legacy(self.alignments, self.images.input_images, arguments.input_aligned_dir)
|
|
|
|
self.post_process = PostProcess(arguments)
|
|
self.verify_output = False
|
|
|
|
self.opts = OptionalActions(self.args, self.images.input_images, self.alignments)
|
|
logger.debug("Initialized %s", self.__class__.__name__)
|
|
|
|
def process(self):
|
|
""" Original & LowMem models go with converter
|
|
|
|
Note: GAN prediction outputs a mask + an image, while other
|
|
predicts only an image. """
|
|
Utils.set_verbosity(self.args.loglevel)
|
|
|
|
if not self.alignments.have_alignments_file:
|
|
self.load_extractor()
|
|
|
|
model = self.load_model()
|
|
converter = self.load_converter(model)
|
|
|
|
batch = BackgroundGenerator(self.prepare_images(), 1)
|
|
|
|
for item in batch.iterator():
|
|
self.convert(converter, item)
|
|
|
|
if self.extract_faces:
|
|
queue_manager.terminate_queues()
|
|
|
|
Utils.finalize(self.images.images_found,
|
|
self.faces_count,
|
|
self.verify_output)
|
|
|
|
def load_extractor(self):
|
|
""" Set on the fly extraction """
|
|
logger.warning("No Alignments file found. Extracting on the fly.")
|
|
logger.warning("NB: This will use the inferior dlib-hog for extraction "
|
|
"and dlib pose predictor for landmarks. It is recommended "
|
|
"to perfom Extract first for superior results")
|
|
for task in ("load", "detect", "align"):
|
|
queue_manager.add_queue(task, maxsize=0)
|
|
|
|
detector = PluginLoader.get_detector("dlib_hog")(loglevel=self.args.loglevel)
|
|
aligner = PluginLoader.get_aligner("dlib")(loglevel=self.args.loglevel)
|
|
|
|
d_kwargs = {"in_queue": queue_manager.get_queue("load"),
|
|
"out_queue": queue_manager.get_queue("detect")}
|
|
a_kwargs = {"in_queue": queue_manager.get_queue("detect"),
|
|
"out_queue": queue_manager.get_queue("align")}
|
|
|
|
d_process = SpawnProcess(detector.run, **d_kwargs)
|
|
d_event = d_process.event
|
|
d_process.start()
|
|
|
|
a_process = SpawnProcess(aligner.run, **a_kwargs)
|
|
a_event = a_process.event
|
|
a_process.start()
|
|
|
|
d_event.wait(10)
|
|
if not d_event.is_set():
|
|
raise ValueError("Error inititalizing Detector")
|
|
a_event.wait(10)
|
|
if not a_event.is_set():
|
|
raise ValueError("Error inititalizing Aligner")
|
|
|
|
self.extract_faces = True
|
|
|
|
def load_model(self):
|
|
""" Load the model requested for conversion """
|
|
logger.debug("Loading Model")
|
|
model_dir = get_folder(self.args.model_dir)
|
|
model = PluginLoader.get_model(self.args.trainer)(model_dir, self.args.gpus, predict=True)
|
|
logger.debug("Loaded Model")
|
|
return model
|
|
|
|
def load_converter(self, model):
|
|
""" Load the requested converter for conversion """
|
|
conv = self.args.converter
|
|
converter = PluginLoader.get_converter(conv)(
|
|
model.converter(self.args.swap_model),
|
|
model=model,
|
|
arguments=self.args)
|
|
return converter
|
|
|
|
def prepare_images(self):
|
|
""" Prepare the images for conversion """
|
|
filename = ""
|
|
for filename, image in tqdm(self.images.load(),
|
|
total=self.images.images_found,
|
|
file=sys.stdout):
|
|
|
|
if (self.args.discard_frames and
|
|
self.opts.check_skipframe(filename) == "discard"):
|
|
continue
|
|
|
|
frame = os.path.basename(filename)
|
|
if self.extract_faces:
|
|
detected_faces = self.detect_faces(filename, image)
|
|
else:
|
|
detected_faces = self.alignments_faces(frame, image)
|
|
|
|
faces_count = len(detected_faces)
|
|
if faces_count != 0:
|
|
# Post processing requires a dict with "detected_faces" key
|
|
self.post_process.do_actions(
|
|
{"detected_faces": detected_faces})
|
|
self.faces_count += faces_count
|
|
|
|
if faces_count > 1:
|
|
self.verify_output = True
|
|
logger.verbose("Found more than one face in "
|
|
"an image! '%s'", frame)
|
|
|
|
yield filename, image, detected_faces
|
|
|
|
@staticmethod
|
|
def detect_faces(filename, image):
|
|
""" Extract the face from a frame (If not alignments file found) """
|
|
queue_manager.get_queue("load").put((filename, image))
|
|
item = queue_manager.get_queue("align").get()
|
|
detected_faces = item["detected_faces"]
|
|
return detected_faces
|
|
|
|
def alignments_faces(self, frame, image):
|
|
""" Get the face from alignments file """
|
|
if not self.check_alignments(frame):
|
|
return list()
|
|
|
|
faces = self.alignments.get_faces_in_frame(frame)
|
|
detected_faces = list()
|
|
|
|
for rawface in faces:
|
|
face = DetectedFace()
|
|
face.from_alignment(rawface, image=image)
|
|
detected_faces.append(face)
|
|
return detected_faces
|
|
|
|
def check_alignments(self, frame):
|
|
""" If we have no alignments for this image, skip it """
|
|
have_alignments = self.alignments.frame_exists(frame)
|
|
if not have_alignments:
|
|
tqdm.write("No alignment found for {}, "
|
|
"skipping".format(frame))
|
|
return have_alignments
|
|
|
|
def convert(self, converter, item):
|
|
""" Apply the conversion transferring faces onto frames """
|
|
try:
|
|
filename, image, faces = item
|
|
skip = self.opts.check_skipframe(filename)
|
|
|
|
if not skip:
|
|
for face in faces:
|
|
image = converter.patch_image(image, face)
|
|
filename = str(self.output_dir / Path(filename).name)
|
|
cv2.imwrite(filename, image) # pylint: disable=no-member
|
|
except Exception as err:
|
|
logger.error("Failed to convert image: '%s'. Reason: %s", filename, err)
|
|
raise
|
|
|
|
|
|
class OptionalActions():
|
|
""" Process the optional actions for convert """
|
|
|
|
def __init__(self, args, input_images, alignments):
|
|
logger.debug("Initializing %s", self.__class__.__name__)
|
|
self.args = args
|
|
self.input_images = input_images
|
|
self.alignments = alignments
|
|
self.frame_ranges = self.get_frame_ranges()
|
|
self.imageidxre = re.compile(r"[^(mp4)](\d+)(?!.*\d)")
|
|
|
|
self.remove_skipped_faces()
|
|
logger.debug("Initialized %s", self.__class__.__name__)
|
|
|
|
# SKIP FACES #
|
|
def remove_skipped_faces(self):
|
|
""" Remove deleted faces from the loaded alignments """
|
|
logger.debug("Filtering Faces")
|
|
face_hashes = self.get_face_hashes()
|
|
if not face_hashes:
|
|
logger.debug("No face hashes. Not skipping any faces")
|
|
return
|
|
pre_face_count = self.alignments.faces_count
|
|
self.alignments.filter_hashes(face_hashes, filter_out=False)
|
|
logger.info("Faces filtered out: %s", pre_face_count - self.alignments.faces_count)
|
|
|
|
def get_face_hashes(self):
|
|
""" Check for the existence of an aligned directory for identifying
|
|
which faces in the target frames should be swapped.
|
|
If it exists, obtain the hashes of the faces in the folder """
|
|
face_hashes = list()
|
|
input_aligned_dir = self.args.input_aligned_dir
|
|
|
|
if input_aligned_dir is None:
|
|
logger.verbose("Aligned directory not specified. All faces listed in the "
|
|
"alignments file will be converted")
|
|
elif not os.path.isdir(input_aligned_dir):
|
|
logger.warning("Aligned directory not found. All faces listed in the "
|
|
"alignments file will be converted")
|
|
else:
|
|
file_list = [path for path in get_image_paths(input_aligned_dir)]
|
|
logger.info("Getting Face Hashes for selected Aligned Images")
|
|
for face in tqdm(file_list, desc="Hashing Faces"):
|
|
face_hashes.append(hash_image_file(face))
|
|
logger.debug("Face Hashes: %s", (len(face_hashes)))
|
|
if not face_hashes:
|
|
logger.error("Aligned directory is empty, no faces will be converted!")
|
|
exit(1)
|
|
elif len(face_hashes) <= len(self.input_images) / 3:
|
|
logger.warning("Aligned directory contains far fewer images than the input "
|
|
"directory, are you sure this is the right folder?")
|
|
return face_hashes
|
|
|
|
# SKIP FRAME RANGES #
|
|
def get_frame_ranges(self):
|
|
""" split out the frame ranges and parse out 'min' and 'max' values """
|
|
if not self.args.frame_ranges:
|
|
return None
|
|
|
|
minmax = {"min": 0, # never any frames less than 0
|
|
"max": float("inf")}
|
|
rng = [tuple(map(lambda q: minmax[q] if q in minmax.keys() else int(q),
|
|
v.split("-")))
|
|
for v in self.args.frame_ranges]
|
|
return rng
|
|
|
|
def check_skipframe(self, filename):
|
|
""" Check whether frame is to be skipped """
|
|
if not self.frame_ranges:
|
|
return None
|
|
idx = int(self.imageidxre.findall(filename)[0])
|
|
skipframe = not any(map(lambda b: b[0] <= idx <= b[1],
|
|
self.frame_ranges))
|
|
if skipframe and self.args.discard_frames:
|
|
skipframe = "discard"
|
|
return skipframe
|
|
|
|
|
|
class Legacy():
|
|
""" Update legacy alignments:
|
|
- Rotate landmarks and bounding boxes on legacy alignments
|
|
and remove the 'r' parameter
|
|
- Add face hashes to alignments file
|
|
"""
|
|
def __init__(self, alignments, frames, faces_dir):
|
|
self.alignments = alignments
|
|
self.frames = {os.path.basename(frame): frame
|
|
for frame in frames}
|
|
self.process(faces_dir)
|
|
|
|
def process(self, faces_dir):
|
|
""" Run the rotate alignments process """
|
|
rotated = self.alignments.get_legacy_rotation()
|
|
hashes = self.alignments.get_legacy_no_hashes()
|
|
if not rotated and not hashes:
|
|
return
|
|
if rotated:
|
|
logger.info("Legacy rotated frames found. Converting...")
|
|
self.rotate_landmarks(rotated)
|
|
self.alignments.save()
|
|
if hashes and faces_dir:
|
|
logger.info("Legacy alignments found. Adding Face Hashes...")
|
|
self.add_hashes(hashes, faces_dir)
|
|
self.alignments.save()
|
|
|
|
def rotate_landmarks(self, rotated):
|
|
""" Rotate the landmarks """
|
|
for rotate_item in tqdm(rotated, desc="Rotating Landmarks"):
|
|
frame = self.frames.get(rotate_item, None)
|
|
if frame is None:
|
|
logger.debug("Skipping missing frame: '%s'", rotate_item)
|
|
continue
|
|
self.alignments.rotate_existing_landmarks(rotate_item, frame)
|
|
|
|
def add_hashes(self, hashes, faces_dir):
|
|
""" Add Face Hashes to the alignments file """
|
|
all_faces = dict()
|
|
face_files = sorted(face for face in os.listdir(faces_dir) if "_" in face)
|
|
for face in face_files:
|
|
filename, extension = os.path.splitext(face)
|
|
index = filename[filename.rfind("_") + 1:]
|
|
if not index.isdigit():
|
|
continue
|
|
orig_frame = filename[:filename.rfind("_")] + extension
|
|
all_faces.setdefault(orig_frame, dict())[int(index)] = os.path.join(faces_dir, face)
|
|
|
|
for frame in tqdm(hashes):
|
|
if frame not in all_faces.keys():
|
|
logger.warning("Skipping missing frame: '%s'", frame)
|
|
continue
|
|
hash_faces = all_faces[frame]
|
|
for index, face_path in hash_faces.items():
|
|
hash_faces[index] = hash_image_file(face_path)
|
|
self.alignments.add_face_hashes(frame, hash_faces)
|